diff --git a/.gitignore b/.gitignore index 69e62f1c..2494dfad 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ coverage .tmp -debug-dump.txt \ No newline at end of file +debug-dump.txt +.idea/ diff --git a/typescript/index.js b/typescript/index.js index 222838bf..dfaf90ee 100644 --- a/typescript/index.js +++ b/typescript/index.js @@ -14,6 +14,7 @@ module.exports = new Package('typescript', [require('../jsdoc')]) .factory(require('./services/tsParser/getContent')) .factory(require('./services/convertPrivateClassesToInterfaces')) +.factory(require('./services/typescript-symbol-map')) .factory('EXPORT_DOC_TYPES', function() { return [ @@ -35,6 +36,7 @@ module.exports = new Package('typescript', [require('../jsdoc')]) // Register the processors .processor(require('./processors/readTypeScriptModules')) +.processor(require('./processors/linkInheritedDocs')) // Configure ids and paths .config(function(computeIdsProcessor, computePathsProcessor, EXPORT_DOC_TYPES) { diff --git a/typescript/mocks/readTypeScriptModules/inheritedMembers.ts b/typescript/mocks/readTypeScriptModules/inheritedMembers.ts new file mode 100644 index 00000000..eb051db8 --- /dev/null +++ b/typescript/mocks/readTypeScriptModules/inheritedMembers.ts @@ -0,0 +1,23 @@ +export class LastParent implements TestInterface { + lastParentProp: number = 0; +} + +export class Child extends FirstParent implements TestInterface { + childProp: boolean = false; +} + +/** + * To ensure that Dgeni properly recognizes classes from exports that will be parsed later, + * the `FirstParent` class will be ordered after the `Child` class. + **/ +export class FirstParent extends LastParent implements TestInterface { + firstParentProp: string = 'Works'; + _privateProp: string = 'Private'; + private privateProp: string = 'Private'; +} + +/** + * This is just a test interface that has been added to ensure that Dgeni only recognizes + * extends heritages. + **/ +export interface TestInterface {} diff --git a/typescript/processors/linkInheritedDocs.js b/typescript/processors/linkInheritedDocs.js new file mode 100644 index 00000000..57b84e39 --- /dev/null +++ b/typescript/processors/linkInheritedDocs.js @@ -0,0 +1,18 @@ +/** + * Processor that links the inherited symbols to the associating dgeni doc. + **/ +module.exports = function linkInheritedDocs(typescriptSymbolMap) { + return { + $runAfter: ['readTypeScriptModules'], + $runBefore: ['parsing-tags'], + $process: docs => { + // Iterate through all docs and link the doc symbols if present. + docs.filter(doc => doc.inheritedSymbols).forEach(doc => linkDocSymbols(doc)) + } + }; + + function linkDocSymbols(doc) { + doc.inheritedDocs = []; + doc.inheritedSymbols.forEach(symbol => doc.inheritedDocs.push(typescriptSymbolMap.get(symbol))); + } +}; diff --git a/typescript/processors/linkInheritedDocs.spec.js b/typescript/processors/linkInheritedDocs.spec.js new file mode 100644 index 00000000..6cd9ee54 --- /dev/null +++ b/typescript/processors/linkInheritedDocs.spec.js @@ -0,0 +1,65 @@ +const mockPackage = require('../mocks/mockPackage'); +const Dgeni = require('dgeni'); +const path = require('canonical-path'); + +describe('linkInheritedDocs', function() { + + let dgeni, injector, tsProcessor, linkProcessor = null; + + beforeEach(function () { + dgeni = new Dgeni([mockPackage()]); + injector = dgeni.configureInjector(); + + tsProcessor = injector.get('readTypeScriptModules'); + linkProcessor = injector.get('linkInheritedDocs'); + + // Since the `readTypeScriptModules` mock folder includes the a good amount files, the + // spec for linking the inherited docs will just use those. + tsProcessor.basePath = path.resolve(__dirname, '../mocks/readTypeScriptModules'); + }); + + it('should properly link the inherited docs', () => { + let docsArray = []; + + tsProcessor.sourceFiles = ['inheritedMembers.ts']; + + tsProcessor.$process(docsArray); + linkProcessor.$process(docsArray); + + let childDoc = docsArray[3]; + let firstParentDoc = docsArray[5]; + let lastParentDoc = docsArray[1]; + + expect(childDoc.inheritedDocs).toEqual([firstParentDoc]); + expect(firstParentDoc.inheritedDocs).toEqual([lastParentDoc]); + expect(lastParentDoc.inheritedDocs).toEqual([]); + }); + + it('should properly resolve members in inherited docs', () => { + let docsArray = []; + + tsProcessor.sourceFiles = ['inheritedMembers.ts']; + + tsProcessor.$process(docsArray); + linkProcessor.$process(docsArray); + + let childDoc = docsArray[3]; + let members = getInheritedMembers(childDoc); + + expect(members.length).toBe(3); + expect(members[0].name).toBe('childProp'); + expect(members[1].name).toBe('firstParentProp'); + expect(members[2].name).toBe('lastParentProp'); + }); + + /** Returns a list of all inherited members of a doc. */ + function getInheritedMembers(doc) { + let members = doc.members || []; + + doc.inheritedDocs.forEach(inheritedDoc => { + members = members.concat(getInheritedMembers(inheritedDoc)); + }); + + return members; + } +}); diff --git a/typescript/processors/readTypeScriptModules.js b/typescript/processors/readTypeScriptModules.js index 613ba236..245e0591 100644 --- a/typescript/processors/readTypeScriptModules.js +++ b/typescript/processors/readTypeScriptModules.js @@ -3,8 +3,9 @@ var path = require('canonical-path'); var _ = require('lodash'); var ts = require('typescript'); -module.exports = function readTypeScriptModules(tsParser, modules, getFileInfo, ignoreTypeScriptNamespaces, - getExportDocType, getExportAccessibility, getContent, createDocMessage, log) { +module.exports = function readTypeScriptModules( + tsParser, modules, getFileInfo, ignoreTypeScriptNamespaces, getExportDocType, + getExportAccessibility, getContent, createDocMessage, log, typescriptSymbolMap) { return { $runAfter: ['files-read'], @@ -41,6 +42,7 @@ module.exports = function readTypeScriptModules(tsParser, modules, getFileInfo, var filesPaths = expandSourceFiles(this.sourceFiles, basePath); var parseInfo = tsParser.parse(filesPaths, this.basePath); var moduleSymbols = parseInfo.moduleSymbols; + var typeChecker = parseInfo.typeChecker; // Iterate through each of the modules that were parsed and generate a module doc // as well as docs for each module's exports. @@ -67,7 +69,7 @@ module.exports = function readTypeScriptModules(tsParser, modules, getFileInfo, // TODO: find a way of generating docs for them if (!resolvedExport.declarations) return; - var exportDoc = createExportDoc(exportSymbol.name, resolvedExport, moduleDoc, basePath, parseInfo.typeChecker); + var exportDoc = createExportDoc(exportSymbol.name, resolvedExport, moduleDoc, basePath, typeChecker); log.debug('>>>> EXPORT: ' + exportDoc.name + ' (' + exportDoc.docType + ') from ' + moduleDoc.id); // Add this export doc to its module doc @@ -77,6 +79,12 @@ module.exports = function readTypeScriptModules(tsParser, modules, getFileInfo, exportDoc.members = []; exportDoc.statics = []; + // Store the dgeni doc of the resolved symbol in the typescriptSymbolMap. + typescriptSymbolMap.set(resolvedExport, exportDoc); + + // Resolve all inherited symbols and store them inside of the exportDoc object. + exportDoc.inheritedSymbols = resolveInheritedSymbols(resolvedExport, typeChecker); + // Generate docs for each of the export's members if (resolvedExport.flags & ts.SymbolFlags.HasMembers) { @@ -87,7 +95,7 @@ module.exports = function readTypeScriptModules(tsParser, modules, getFileInfo, } log.silly('>>>>>> member: ' + memberName + ' from ' + exportDoc.id + ' in ' + moduleDoc.id); var memberSymbol = resolvedExport.members[memberName]; - var memberDoc = createMemberDoc(memberSymbol, exportDoc, basePath, parseInfo.typeChecker); + var memberDoc = createMemberDoc(memberSymbol, exportDoc, basePath, typeChecker); // We special case the constructor and sort the other members alphabetically if (memberSymbol.flags & ts.SymbolFlags.Constructor) { @@ -143,6 +151,42 @@ module.exports = function readTypeScriptModules(tsParser, modules, getFileInfo, } }; + /** + * Resolves all inherited class symbols of the specified symbol. + * @param {ts.Symbol} symbol ClassDeclaration symbol with members + * @param {ts.TypeChecker} typeChecker TypeScript TypeChecker for symbols + * @returns {Symbol[]} List of inherited class symbols. + **/ + function resolveInheritedSymbols(symbol, typeChecker) { + var declaration = symbol.valueDeclaration || symbol.declarations[0]; + var symbols = []; + + if (declaration && declaration.heritageClauses) { + symbols = declaration.heritageClauses + // Filter for extends heritage clauses. + .filter(isExtendsHeritage) + // Resolve an array of the inherited symbols from the heritage clause. + .map(heritage => convertHeritageClauseToSymbols(heritage, typeChecker)) + // Flatten the arrays of inherited symbols. + .reduce((symbols, cur) => symbols.concat(cur), []); + } + + return symbols; + } + + /** + * Converts a heritage clause into a list of symbols + * @param {ts.HeritageClause} heritage + * @param {ts.TypeChecker} typeChecker + * @returns {ts.SymbolTable} List of heritage symbols. + **/ + function convertHeritageClauseToSymbols(heritage, typeChecker) { + var heritages = heritage.types.map(expression => + typeChecker.getTypeAtLocation(expression).getSymbol() + ); + + return heritages.reduce((inheritances, current) => inheritances.concat(current), []); + } function createModuleDoc(moduleSymbol, basePath) { var id = moduleSymbol.name.replace(/^"|"$/g, ''); @@ -189,7 +233,7 @@ module.exports = function readTypeScriptModules(tsParser, modules, getFileInfo, if (declaration.heritageClauses) { declaration.heritageClauses.forEach(function(heritage) { - if (heritage.token == ts.SyntaxKind.ExtendsKeyword) { + if (isExtendsHeritage(heritage)) { heritageString += " extends"; heritage.types.forEach(function(typ, idx) { heritageString += (idx > 0 ? ',' : '') + typ.getFullText(); @@ -450,6 +494,10 @@ module.exports = function readTypeScriptModules(tsParser, modules, getFileInfo, return location; } + /** Checks if the specified heritage clause is an extends keyword. */ + function isExtendsHeritage(heritageClause) { + return heritageClause.token === ts.SyntaxKind.ExtendsKeyword + } }; function convertToRegexCollection(items) { diff --git a/typescript/processors/readTypeScriptModules.spec.js b/typescript/processors/readTypeScriptModules.spec.js index b5132c8c..d52b1a9a 100644 --- a/typescript/processors/readTypeScriptModules.spec.js +++ b/typescript/processors/readTypeScriptModules.spec.js @@ -57,6 +57,28 @@ describe('readTypeScriptModules', function() { }); + describe('inherited symbols', function() { + + it('should add the list of inherited symbols to a class doc', function() { + processor.sourceFiles = [ 'inheritedMembers.ts' ]; + var docs = []; + + processor.$process(docs); + + var childDoc = docs[3]; + var firstParentDoc = docs[5]; + var lastParentDoc = docs[1]; + + expect(childDoc.inheritedSymbols.length).toBe(1); + expect(childDoc.inheritedSymbols[0]).toBe(firstParentDoc.exportSymbol); + + expect(firstParentDoc.inheritedSymbols.length).toBe(1); + expect(firstParentDoc.inheritedSymbols[0]).toBe(lastParentDoc.exportSymbol); + + expect(lastParentDoc.inheritedSymbols.length).toBe(0); + }); + + }); describe('ignoreExportsMatching', function() { it('should ignore exports that match items in the `ignoreExportsMatching` property', function() { @@ -235,4 +257,4 @@ describe('readTypeScriptModules', function() { function getNames(collection) { return collection.map(function(item) { return item.name; }); -} \ No newline at end of file +} diff --git a/typescript/services/typescript-symbol-map.js b/typescript/services/typescript-symbol-map.js new file mode 100644 index 00000000..4fe7efc1 --- /dev/null +++ b/typescript/services/typescript-symbol-map.js @@ -0,0 +1,6 @@ +/** + * Service that can be used to store typescript symbols with their associated dgeni doc. + **/ +module.exports = function typescriptSymbolMap() { + return new Map(); +};