2018-11-21 22:49:08 +00:00
|
|
|
const ts = require('typescript');
|
|
|
|
const path = require('path');
|
|
|
|
const Documentation = require('./Documentation');
|
|
|
|
module.exports = checkSources;
|
|
|
|
|
2017-07-28 08:09:26 +00:00
|
|
|
/**
|
2018-11-21 22:49:08 +00:00
|
|
|
* @param {!Array<!import('../Source')>} sources
|
2017-07-28 08:09:26 +00:00
|
|
|
*/
|
2018-11-21 22:49:08 +00:00
|
|
|
function checkSources(sources) {
|
2019-01-15 03:57:05 +00:00
|
|
|
// special treatment for Events.js
|
|
|
|
const classEvents = new Map();
|
2020-05-07 10:54:55 +00:00
|
|
|
const eventsSource = sources.find((source) => source.name() === 'Events.js');
|
2019-01-15 03:57:05 +00:00
|
|
|
if (eventsSource) {
|
2020-05-07 10:54:55 +00:00
|
|
|
const { Events } = require(eventsSource.filePath());
|
2019-01-15 03:57:05 +00:00
|
|
|
for (const [className, events] of Object.entries(Events))
|
2020-05-07 10:54:55 +00:00
|
|
|
classEvents.set(
|
|
|
|
className,
|
|
|
|
Array.from(Object.values(events))
|
|
|
|
.filter((e) => typeof e === 'string')
|
|
|
|
.map((e) => Documentation.Member.createEvent(e))
|
|
|
|
);
|
2019-01-15 03:57:05 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
const excludeClasses = new Set([]);
|
|
|
|
const program = ts.createProgram({
|
|
|
|
options: {
|
|
|
|
allowJs: true,
|
2020-05-07 10:54:55 +00:00
|
|
|
target: ts.ScriptTarget.ES2017,
|
2018-11-21 22:49:08 +00:00
|
|
|
},
|
2020-05-07 10:54:55 +00:00
|
|
|
rootNames: sources.map((source) => source.filePath()),
|
2018-11-21 22:49:08 +00:00
|
|
|
});
|
|
|
|
const checker = program.getTypeChecker();
|
|
|
|
const sourceFiles = program.getSourceFiles();
|
|
|
|
/** @type {!Array<!Documentation.Class>} */
|
|
|
|
const classes = [];
|
|
|
|
/** @type {!Map<string, string>} */
|
|
|
|
const inheritance = new Map();
|
2020-04-16 13:59:28 +00:00
|
|
|
|
2020-05-07 10:54:55 +00:00
|
|
|
const sourceFilesNoNodeModules = sourceFiles.filter(
|
|
|
|
(x) => !x.fileName.includes('node_modules')
|
|
|
|
);
|
|
|
|
const sourceFileNamesSet = new Set(
|
|
|
|
sourceFilesNoNodeModules.map((x) => x.fileName)
|
|
|
|
);
|
|
|
|
sourceFilesNoNodeModules.map((x) => {
|
2020-04-16 13:59:28 +00:00
|
|
|
if (x.fileName.includes('/lib/')) {
|
2020-05-07 10:54:55 +00:00
|
|
|
const potentialTSSource = x.fileName
|
|
|
|
.replace('lib', 'src')
|
|
|
|
.replace('.js', '.ts');
|
2020-04-16 13:59:28 +00:00
|
|
|
if (sourceFileNamesSet.has(potentialTSSource)) {
|
|
|
|
/* Not going to visit this file because we have the TypeScript src code
|
|
|
|
* which we'll use instead.
|
|
|
|
*/
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
visit(x);
|
|
|
|
});
|
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
const errors = [];
|
2020-05-07 10:54:55 +00:00
|
|
|
const documentation = new Documentation(
|
|
|
|
recreateClassesWithInheritance(classes, inheritance)
|
|
|
|
);
|
2019-01-15 03:57:05 +00:00
|
|
|
|
2020-05-07 10:54:55 +00:00
|
|
|
return { errors, documentation };
|
2017-07-28 08:09:26 +00:00
|
|
|
|
2018-04-04 21:06:21 +00:00
|
|
|
/**
|
2018-11-21 22:49:08 +00:00
|
|
|
* @param {!Array<!Documentation.Class>} classes
|
|
|
|
* @param {!Map<string, string>} inheritance
|
2020-06-12 10:38:24 +00:00
|
|
|
* @returns {!Array<!Documentation.Class>}
|
2018-04-04 21:06:21 +00:00
|
|
|
*/
|
2018-11-21 22:49:08 +00:00
|
|
|
function recreateClassesWithInheritance(classes, inheritance) {
|
2020-05-07 10:54:55 +00:00
|
|
|
const classesByName = new Map(classes.map((cls) => [cls.name, cls]));
|
|
|
|
return classes.map((cls) => {
|
2018-11-21 22:49:08 +00:00
|
|
|
const membersMap = new Map();
|
|
|
|
for (let wp = cls; wp; wp = classesByName.get(inheritance.get(wp.name))) {
|
|
|
|
for (const member of wp.membersArray) {
|
|
|
|
// Member was overridden.
|
|
|
|
const memberId = member.kind + ':' + member.name;
|
2020-05-07 10:54:55 +00:00
|
|
|
if (membersMap.has(memberId)) continue;
|
2018-11-21 22:49:08 +00:00
|
|
|
membersMap.set(memberId, member);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new Documentation.Class(cls.name, Array.from(membersMap.values()));
|
2017-07-07 16:36:45 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
/**
|
|
|
|
* @param {!ts.Node} node
|
|
|
|
*/
|
|
|
|
function visit(node) {
|
|
|
|
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
2020-05-07 10:54:55 +00:00
|
|
|
const symbol = node.name
|
|
|
|
? checker.getSymbolAtLocation(node.name)
|
|
|
|
: node.symbol;
|
2018-11-21 22:49:08 +00:00
|
|
|
let className = symbol.getName();
|
|
|
|
|
|
|
|
if (className === '__class') {
|
|
|
|
let parent = node;
|
2020-05-07 10:54:55 +00:00
|
|
|
while (parent.parent) parent = parent.parent;
|
|
|
|
className = path.basename(parent.fileName, '.js');
|
2018-11-21 22:49:08 +00:00
|
|
|
}
|
|
|
|
if (className && !excludeClasses.has(className)) {
|
|
|
|
classes.push(serializeClass(className, symbol, node));
|
|
|
|
const parentClassName = parentClass(node);
|
2020-05-07 10:54:55 +00:00
|
|
|
if (parentClassName) inheritance.set(className, parentClassName);
|
2018-11-21 22:49:08 +00:00
|
|
|
excludeClasses.add(className);
|
|
|
|
}
|
2017-07-21 07:58:38 +00:00
|
|
|
}
|
2018-11-21 22:49:08 +00:00
|
|
|
ts.forEachChild(node, visit);
|
2017-07-12 15:01:21 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
function parentClass(classNode) {
|
|
|
|
for (const herigateClause of classNode.heritageClauses || []) {
|
|
|
|
for (const heritageType of herigateClause.types) {
|
|
|
|
const parentClassName = heritageType.expression.escapedText;
|
|
|
|
return parentClassName;
|
|
|
|
}
|
2017-07-14 20:03:21 +00:00
|
|
|
}
|
2018-11-21 22:49:08 +00:00
|
|
|
return null;
|
2017-07-14 20:03:21 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
function serializeSymbol(symbol, circular = []) {
|
2020-05-07 10:54:55 +00:00
|
|
|
const type = checker.getTypeOfSymbolAtLocation(
|
|
|
|
symbol,
|
|
|
|
symbol.valueDeclaration
|
|
|
|
);
|
2018-11-21 22:49:08 +00:00
|
|
|
const name = symbol.getName();
|
2020-03-31 08:48:09 +00:00
|
|
|
if (symbol.valueDeclaration && symbol.valueDeclaration.dotDotDotToken) {
|
2018-11-21 22:49:08 +00:00
|
|
|
const innerType = serializeType(type.typeArguments[0], circular);
|
|
|
|
innerType.name = '...' + innerType.name;
|
|
|
|
return Documentation.Member.createProperty('...' + name, innerType);
|
|
|
|
}
|
2020-05-07 10:54:55 +00:00
|
|
|
return Documentation.Member.createProperty(
|
|
|
|
name,
|
|
|
|
serializeType(type, circular)
|
|
|
|
);
|
2017-07-07 16:36:45 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
/**
|
|
|
|
* @param {!ts.ObjectType} type
|
|
|
|
*/
|
|
|
|
function isRegularObject(type) {
|
2020-05-07 10:54:55 +00:00
|
|
|
if (type.isIntersection()) return true;
|
|
|
|
if (!type.objectFlags) return false;
|
|
|
|
if (!('aliasSymbol' in type)) return false;
|
|
|
|
if (type.getConstructSignatures().length) return false;
|
|
|
|
if (type.getCallSignatures().length) return false;
|
|
|
|
if (type.isLiteral()) return false;
|
|
|
|
if (type.isUnion()) return false;
|
2020-03-31 08:48:09 +00:00
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
return true;
|
2017-07-14 20:03:21 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
/**
|
|
|
|
* @param {!ts.Type} type
|
2020-06-12 10:38:24 +00:00
|
|
|
* @returns {!Documentation.Type}
|
2018-11-21 22:49:08 +00:00
|
|
|
*/
|
|
|
|
function serializeType(type, circular = []) {
|
|
|
|
let typeName = checker.typeToString(type);
|
2020-05-07 10:54:55 +00:00
|
|
|
if (
|
|
|
|
typeName === 'any' ||
|
|
|
|
typeName === '{ [x: string]: string; }' ||
|
|
|
|
typeName === '{}'
|
|
|
|
)
|
2018-11-21 22:49:08 +00:00
|
|
|
typeName = 'Object';
|
|
|
|
const nextCircular = [typeName].concat(circular);
|
|
|
|
|
|
|
|
if (isRegularObject(type)) {
|
|
|
|
let properties = undefined;
|
|
|
|
if (!circular.includes(typeName))
|
2020-05-07 10:54:55 +00:00
|
|
|
properties = type
|
|
|
|
.getProperties()
|
|
|
|
.map((property) => serializeSymbol(property, nextCircular));
|
2018-11-21 22:49:08 +00:00
|
|
|
return new Documentation.Type('Object', properties);
|
|
|
|
}
|
|
|
|
if (type.isUnion() && typeName.includes('|')) {
|
2020-05-07 10:54:55 +00:00
|
|
|
const types = type.types.map((type) => serializeType(type, circular));
|
|
|
|
const name = types.map((type) => type.name).join('|');
|
|
|
|
const properties = [].concat(...types.map((type) => type.properties));
|
|
|
|
return new Documentation.Type(
|
|
|
|
name.replace(/false\|true/g, 'boolean'),
|
|
|
|
properties
|
|
|
|
);
|
2018-11-21 22:49:08 +00:00
|
|
|
}
|
|
|
|
if (type.typeArguments) {
|
|
|
|
const properties = [];
|
|
|
|
const innerTypeNames = [];
|
|
|
|
for (const typeArgument of type.typeArguments) {
|
|
|
|
const innerType = serializeType(typeArgument, nextCircular);
|
2020-05-07 10:54:55 +00:00
|
|
|
if (innerType.properties) properties.push(...innerType.properties);
|
2018-11-21 22:49:08 +00:00
|
|
|
innerTypeNames.push(innerType.name);
|
|
|
|
}
|
2020-05-07 10:54:55 +00:00
|
|
|
if (
|
|
|
|
innerTypeNames.length === 0 ||
|
|
|
|
(innerTypeNames.length === 1 && innerTypeNames[0] === 'void')
|
|
|
|
)
|
2018-11-21 22:49:08 +00:00
|
|
|
return new Documentation.Type(type.symbol.name);
|
2020-05-07 10:54:55 +00:00
|
|
|
return new Documentation.Type(
|
|
|
|
`${type.symbol.name}<${innerTypeNames.join(', ')}>`,
|
|
|
|
properties
|
|
|
|
);
|
2018-11-21 22:49:08 +00:00
|
|
|
}
|
|
|
|
return new Documentation.Type(typeName, []);
|
2017-07-07 16:36:45 +00:00
|
|
|
}
|
|
|
|
|
2020-04-22 14:44:04 +00:00
|
|
|
/**
|
|
|
|
* @param {!ts.Symbol} symbol
|
2020-06-12 10:38:24 +00:00
|
|
|
* @returns {boolean}
|
2020-04-22 14:44:04 +00:00
|
|
|
*/
|
|
|
|
function symbolHasPrivateModifier(symbol) {
|
2020-05-07 10:54:55 +00:00
|
|
|
const modifiers =
|
|
|
|
(symbol.valueDeclaration && symbol.valueDeclaration.modifiers) || [];
|
|
|
|
return modifiers.some(
|
|
|
|
(modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword
|
|
|
|
);
|
2020-04-22 14:44:04 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
/**
|
|
|
|
* @param {string} className
|
|
|
|
* @param {!ts.Symbol} symbol
|
2020-06-12 10:38:24 +00:00
|
|
|
* @returns {}
|
2018-11-21 22:49:08 +00:00
|
|
|
*/
|
|
|
|
function serializeClass(className, symbol, node) {
|
|
|
|
/** @type {!Array<!Documentation.Member>} */
|
2019-01-15 03:57:05 +00:00
|
|
|
const members = classEvents.get(className) || [];
|
2017-10-02 20:38:44 +00:00
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
for (const [name, member] of symbol.members || []) {
|
2020-04-22 14:44:04 +00:00
|
|
|
/* Before TypeScript we denoted private methods with an underscore
|
|
|
|
* but in TypeScript we use the private keyword
|
|
|
|
* hence we check for either here.
|
|
|
|
*/
|
2020-05-07 10:54:55 +00:00
|
|
|
if (name.startsWith('_') || symbolHasPrivateModifier(member)) continue;
|
2020-04-22 14:44:04 +00:00
|
|
|
|
2020-05-07 10:54:55 +00:00
|
|
|
const memberType = checker.getTypeOfSymbolAtLocation(
|
|
|
|
member,
|
|
|
|
member.valueDeclaration
|
|
|
|
);
|
2018-11-21 22:49:08 +00:00
|
|
|
const signature = memberType.getCallSignatures()[0];
|
2020-05-07 10:54:55 +00:00
|
|
|
if (signature) members.push(serializeSignature(name, signature));
|
|
|
|
else members.push(serializeProperty(name, memberType));
|
2018-11-21 22:49:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return new Documentation.Class(className, members);
|
2017-07-11 15:57:26 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 22:49:08 +00:00
|
|
|
/**
|
|
|
|
* @param {string} name
|
|
|
|
* @param {!ts.Signature} signature
|
|
|
|
*/
|
|
|
|
function serializeSignature(name, signature) {
|
2020-05-07 10:54:55 +00:00
|
|
|
const parameters = signature.parameters.map((s) => serializeSymbol(s));
|
2018-11-21 22:49:08 +00:00
|
|
|
const returnType = serializeType(signature.getReturnType());
|
2020-05-07 10:54:55 +00:00
|
|
|
return Documentation.Member.createMethod(
|
|
|
|
name,
|
|
|
|
parameters,
|
|
|
|
returnType.name !== 'void' ? returnType : null
|
|
|
|
);
|
2018-11-21 22:49:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} name
|
|
|
|
* @param {!ts.Type} type
|
|
|
|
*/
|
|
|
|
function serializeProperty(name, type) {
|
|
|
|
return Documentation.Member.createProperty(name, serializeType(type));
|
|
|
|
}
|
|
|
|
}
|