const ts = require('typescript'); const path = require('path'); const Documentation = require('./Documentation'); module.exports = checkSources; /** * @param {!Array} sources */ function checkSources(sources) { // special treatment for Events.js const classEvents = new Map(); const eventsSource = sources.find((source) => source.name() === 'Events.js'); if (eventsSource) { const { Events } = require(eventsSource.filePath()); for (const [className, events] of Object.entries(Events)) classEvents.set( className, Array.from(Object.values(events)) .filter((e) => typeof e === 'string') .map((e) => Documentation.Member.createEvent(e)) ); } const excludeClasses = new Set([]); const program = ts.createProgram({ options: { allowJs: true, target: ts.ScriptTarget.ES2017, }, rootNames: sources.map((source) => source.filePath()), }); const checker = program.getTypeChecker(); const sourceFiles = program.getSourceFiles(); /** @type {!Array} */ const classes = []; /** @type {!Map} */ const inheritance = new Map(); const sourceFilesNoNodeModules = sourceFiles.filter( (x) => !x.fileName.includes('node_modules') ); const sourceFileNamesSet = new Set( sourceFilesNoNodeModules.map((x) => x.fileName) ); sourceFilesNoNodeModules.map((x) => { if (x.fileName.includes('/lib/')) { const potentialTSSource = x.fileName .replace('lib', 'src') .replace('.js', '.ts'); 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); }); const errors = []; const documentation = new Documentation( recreateClassesWithInheritance(classes, inheritance) ); return { errors, documentation }; /** * @param {!Array} classes * @param {!Map} inheritance * @return {!Array} */ function recreateClassesWithInheritance(classes, inheritance) { const classesByName = new Map(classes.map((cls) => [cls.name, cls])); return classes.map((cls) => { 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; if (membersMap.has(memberId)) continue; membersMap.set(memberId, member); } } return new Documentation.Class(cls.name, Array.from(membersMap.values())); }); } /** * @param {!ts.Node} node */ function visit(node) { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { const symbol = node.name ? checker.getSymbolAtLocation(node.name) : node.symbol; let className = symbol.getName(); if (className === '__class') { let parent = node; while (parent.parent) parent = parent.parent; className = path.basename(parent.fileName, '.js'); } if (className && !excludeClasses.has(className)) { classes.push(serializeClass(className, symbol, node)); const parentClassName = parentClass(node); if (parentClassName) inheritance.set(className, parentClassName); excludeClasses.add(className); } } ts.forEachChild(node, visit); } function parentClass(classNode) { for (const herigateClause of classNode.heritageClauses || []) { for (const heritageType of herigateClause.types) { const parentClassName = heritageType.expression.escapedText; return parentClassName; } } return null; } function serializeSymbol(symbol, circular = []) { const type = checker.getTypeOfSymbolAtLocation( symbol, symbol.valueDeclaration ); const name = symbol.getName(); if (symbol.valueDeclaration && symbol.valueDeclaration.dotDotDotToken) { const innerType = serializeType(type.typeArguments[0], circular); innerType.name = '...' + innerType.name; return Documentation.Member.createProperty('...' + name, innerType); } return Documentation.Member.createProperty( name, serializeType(type, circular) ); } /** * @param {!ts.ObjectType} type */ function isRegularObject(type) { 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; return true; } /** * @param {!ts.Type} type * @return {!Documentation.Type} */ function serializeType(type, circular = []) { let typeName = checker.typeToString(type); if ( typeName === 'any' || typeName === '{ [x: string]: string; }' || typeName === '{}' ) typeName = 'Object'; const nextCircular = [typeName].concat(circular); if (isRegularObject(type)) { let properties = undefined; if (!circular.includes(typeName)) properties = type .getProperties() .map((property) => serializeSymbol(property, nextCircular)); return new Documentation.Type('Object', properties); } if (type.isUnion() && typeName.includes('|')) { 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 ); } if (type.typeArguments) { const properties = []; const innerTypeNames = []; for (const typeArgument of type.typeArguments) { const innerType = serializeType(typeArgument, nextCircular); if (innerType.properties) properties.push(...innerType.properties); innerTypeNames.push(innerType.name); } if ( innerTypeNames.length === 0 || (innerTypeNames.length === 1 && innerTypeNames[0] === 'void') ) return new Documentation.Type(type.symbol.name); return new Documentation.Type( `${type.symbol.name}<${innerTypeNames.join(', ')}>`, properties ); } return new Documentation.Type(typeName, []); } /** * @param {!ts.Symbol} symbol * @return {boolean} */ function symbolHasPrivateModifier(symbol) { const modifiers = (symbol.valueDeclaration && symbol.valueDeclaration.modifiers) || []; return modifiers.some( (modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword ); } /** * @param {string} className * @param {!ts.Symbol} symbol * @return {} */ function serializeClass(className, symbol, node) { /** @type {!Array} */ const members = classEvents.get(className) || []; for (const [name, member] of symbol.members || []) { /* Before TypeScript we denoted private methods with an underscore * but in TypeScript we use the private keyword * hence we check for either here. */ if (name.startsWith('_') || symbolHasPrivateModifier(member)) continue; const memberType = checker.getTypeOfSymbolAtLocation( member, member.valueDeclaration ); const signature = memberType.getCallSignatures()[0]; if (signature) members.push(serializeSignature(name, signature)); else members.push(serializeProperty(name, memberType)); } return new Documentation.Class(className, members); } /** * @param {string} name * @param {!ts.Signature} signature */ function serializeSignature(name, signature) { const parameters = signature.parameters.map((s) => serializeSymbol(s)); const returnType = serializeType(signature.getReturnType()); return Documentation.Member.createMethod( name, parameters, returnType.name !== 'void' ? returnType : null ); } /** * @param {string} name * @param {!ts.Type} type */ function serializeProperty(name, type) { return Documentation.Member.createProperty(name, serializeType(type)); } }