From f16ca1ce462fb07fba5183f00c74e61c903634d6 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 13 May 2020 17:01:10 +0100 Subject: [PATCH] build(docs-infra): correctly handle "pseudo" classes (#36989) In the code base there are cases where there is, conceptually, a class that is represented by a combination of an `interface` (type declaration) and a `const` (value declaration). For example: ``` export interface SomeClass { count(a?: string): number; } export const: SomeClass = class { someMethod(a: string = ''): number { ... } }; ``` These were being rendered as interfaces and also not correctly showing the descriptions and default parameter values. In this commit such concepts are now rendered as classes. The classes that are affected by this are: * `DebugElement` * `DebugNode` * `Type` * `EventEmitter` * `TestBed` Note that while decorators are also defined in this form they have their own rendering type (`decorator`) and so are not affecte by this. PR Close #36989 --- .../transforms/angular-api-package/index.js | 8 +- .../mocks/methodParameters.ts | 20 ++++ .../processors/mergeParameterInfo.js | 31 ++++++ .../processors/mergeParameterInfo.spec.js | 48 +++++++++ .../processors/processPseudoClasses.js | 27 ++++++ .../processors/processPseudoClasses.spec.js | 97 +++++++++++++++++++ 6 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 aio/tools/transforms/angular-api-package/mocks/methodParameters.ts create mode 100644 aio/tools/transforms/angular-api-package/processors/mergeParameterInfo.js create mode 100644 aio/tools/transforms/angular-api-package/processors/mergeParameterInfo.spec.js create mode 100644 aio/tools/transforms/angular-api-package/processors/processPseudoClasses.js create mode 100644 aio/tools/transforms/angular-api-package/processors/processPseudoClasses.spec.js diff --git a/aio/tools/transforms/angular-api-package/index.js b/aio/tools/transforms/angular-api-package/index.js index eea0910068..da7cdbd3da 100644 --- a/aio/tools/transforms/angular-api-package/index.js +++ b/aio/tools/transforms/angular-api-package/index.js @@ -15,6 +15,8 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage]) // Register the processors + .processor(require('./processors/mergeParameterInfo')) + .processor(require('./processors/processPseudoClasses')) .processor(require('./processors/splitDescription')) .processor(require('./processors/convertPrivateClassesToInterfaces')) .processor(require('./processors/generateApiListDoc')) @@ -76,7 +78,6 @@ module.exports = .config(function( readTypeScriptModules, readFilesProcessor, collectExamples, tsParser, packageContentFileReader) { - // Tell TypeScript how to load modules that start with with `@angular` tsParser.options.paths = {'@angular/*': [API_SOURCE_PATH + '/*']}; tsParser.options.baseUrl = '.'; @@ -181,14 +182,11 @@ module.exports = }) .config(function(filterMembers) { - filterMembers.notAllowedPatterns.push( - /^ɵ/ - ); + filterMembers.notAllowedPatterns.push(/^ɵ/); }) .config(function(computePathsProcessor, EXPORT_DOC_TYPES, generateApiListDoc) { - const API_SEGMENT = 'api'; generateApiListDoc.outputFolder = API_SEGMENT; diff --git a/aio/tools/transforms/angular-api-package/mocks/methodParameters.ts b/aio/tools/transforms/angular-api-package/mocks/methodParameters.ts new file mode 100644 index 0000000000..2772938049 --- /dev/null +++ b/aio/tools/transforms/angular-api-package/mocks/methodParameters.ts @@ -0,0 +1,20 @@ +export class TestClass { + method1( + /** description of param1 */ param1: number, + /** description of param2 */ param2?: string, + /** description of param3 */ param3: object = {}, + /** description of param4 */ param4 = 'default string', + ) { + /// + } + + /** + * Some description of method 2 + * @param param5 description of param5 + * @param param6 description of param6 + * @param param7 description of param7 + */ + method2(param5: string, param6: number, param7 = 42) { + // + } +} diff --git a/aio/tools/transforms/angular-api-package/processors/mergeParameterInfo.js b/aio/tools/transforms/angular-api-package/processors/mergeParameterInfo.js new file mode 100644 index 0000000000..f07b1beb4e --- /dev/null +++ b/aio/tools/transforms/angular-api-package/processors/mergeParameterInfo.js @@ -0,0 +1,31 @@ +/** + * @dgProcessor + * + * @description + * Merge the description from `@param` tags into the parameter docs + * extracted from the TypeScript + * + * This is actually an override of the processor provided by the `typescript` dgeni package. + * The original does not set the `defaultValue`. + */ +module.exports = function mergeParameterInfo() { + return { + $runAfter: ['readTypeScriptModules', 'tags-extracted'], + $runBefore: ['extra-docs-added'], + $process(docs) { + docs.forEach((doc) => { + if (doc.docType === 'parameter') { + // The `params` property comes from parsing the `@param` jsdoc tags on the container doc + const paramTag = + doc.container.params && doc.container.params.find(param => param.name === doc.name); + if (paramTag && paramTag.description) { + doc.description = paramTag.description; + if (doc.defaultValue === undefined) { + doc.defaultValue = paramTag.defaultValue; + } + } + } + }); + }, + }; +}; diff --git a/aio/tools/transforms/angular-api-package/processors/mergeParameterInfo.spec.js b/aio/tools/transforms/angular-api-package/processors/mergeParameterInfo.spec.js new file mode 100644 index 0000000000..ffbc7094f3 --- /dev/null +++ b/aio/tools/transforms/angular-api-package/processors/mergeParameterInfo.spec.js @@ -0,0 +1,48 @@ +const Dgeni = require('dgeni'); +const path = require('canonical-path'); +const testPackage = require('../../helpers/test-package'); + +describe('mergeParameterInfo', () => { + let injector; + let tsProcessor; + let mergeParameterInfoProcessor; + let parseTagsProcessor; + let extractTagsProcessor; + + beforeEach(() => { + const dgeni = new Dgeni([testPackage('angular-api-package')]); + injector = dgeni.configureInjector(); + + tsProcessor = injector.get('readTypeScriptModules'); + parseTagsProcessor = injector.get('parseTagsProcessor'); + extractTagsProcessor = injector.get('extractTagsProcessor'); + mergeParameterInfoProcessor = injector.get('mergeParameterInfo'); + tsProcessor.basePath = path.resolve(__dirname, '../mocks'); + tsProcessor.sourceFiles = ['methodParameters.ts']; + }); + + it('should merge the param tags into the parameter docs', () => { + const docsArray = []; + + tsProcessor.$process(docsArray); + parseTagsProcessor.$process(docsArray); + extractTagsProcessor.$process(docsArray); + mergeParameterInfoProcessor.$process(docsArray); + + const param5 = docsArray.find(doc => doc.name === 'param5' && doc.container.name === 'method2'); + expect(param5.id).toEqual('methodParameters/TestClass.method2()~param5'); + expect(param5.description).toEqual('description of param5'); + expect(param5.type).toEqual('string'); + + const param6 = docsArray.find(doc => doc.name === 'param6' && doc.container.name === 'method2'); + expect(param6.id).toEqual('methodParameters/TestClass.method2()~param6'); + expect(param6.description).toEqual('description of param6'); + expect(param6.type).toEqual('number'); + + const param7 = docsArray.find(doc => doc.name === 'param7' && doc.container.name === 'method2'); + expect(param7.id).toEqual('methodParameters/TestClass.method2()~param7'); + expect(param7.description).toEqual('description of param7'); + expect(param7.type).toEqual('number'); + expect(param7.defaultValue).toEqual('42'); + }); +}); diff --git a/aio/tools/transforms/angular-api-package/processors/processPseudoClasses.js b/aio/tools/transforms/angular-api-package/processors/processPseudoClasses.js new file mode 100644 index 0000000000..9a51ce842e --- /dev/null +++ b/aio/tools/transforms/angular-api-package/processors/processPseudoClasses.js @@ -0,0 +1,27 @@ +module.exports = function processPseudoClasses(tsHost) { + return { + $runAfter: ['readTypeScriptModules'], + $runBefore: ['parsing-tags'], + $process(docs) { + docs.forEach(doc => { + if (doc.docType === 'interface' && doc.additionalDeclarations && + doc.additionalDeclarations.length > 0) { + doc.docType = 'class'; + const additionalContent = tsHost.getContent(doc.additionalDeclarations[0]); + if (!doc.content || doc.content === '@publicApi' && additionalContent) { + doc.content = additionalContent; + } + doc.members = doc.members && doc.members.filter(m => { + if (m.isNewMember) { + doc.constructorDoc = m; + doc.constructorDoc.name = 'constructor'; + return false; + } else { + return true; + } + }); + } + }); + } + }; +}; diff --git a/aio/tools/transforms/angular-api-package/processors/processPseudoClasses.spec.js b/aio/tools/transforms/angular-api-package/processors/processPseudoClasses.spec.js new file mode 100644 index 0000000000..3913f6448b --- /dev/null +++ b/aio/tools/transforms/angular-api-package/processors/processPseudoClasses.spec.js @@ -0,0 +1,97 @@ +const testPackage = require('../../helpers/test-package'); +const processorFactory = require('./processPseudoClasses'); +const Dgeni = require('dgeni'); + +describe('processPseudoClasses processor', () => { + it('should be available on the injector', () => { + const dgeni = new Dgeni([testPackage('angular-api-package')]); + const injector = dgeni.configureInjector(); + const processor = injector.get('processPseudoClasses'); + expect(processor.$process).toBeDefined(); + expect(processor.$runAfter).toEqual(['readTypeScriptModules']); + expect(processor.$runBefore).toEqual(['parsing-tags']); + }); + + it('should convert "interface+const" docs to "class" docs', () => { + const processor = processorFactory(jasmine.createSpyObj(['getContent'])); + const docs = [ + {docType: 'module', id: 'a'}, + {docType: 'class', id: 'b'}, + {docType: 'interface', id: 'c'}, + {docType: 'interface', id: 'd', additionalDeclarations: []}, + {docType: 'interface', id: 'e', additionalDeclarations: [{}]}, + {docType: 'const', id: 'f'}, + {docType: 'const', id: 'g', additionalDeclarations: []}, + {docType: 'const', id: 'h', additionalDeclarations: [{}]}, + ]; + processor.$process(docs); + expect(docs).toEqual([ + jasmine.objectContaining({docType: 'module', id: 'a'}), + jasmine.objectContaining({docType: 'class', id: 'b'}), + jasmine.objectContaining({docType: 'interface', id: 'c'}), + jasmine.objectContaining({docType: 'interface', id: 'd'}), + + // This is the only one that changes + jasmine.objectContaining({docType: 'class', id: 'e'}), + + jasmine.objectContaining({docType: 'const', id: 'f'}), + jasmine.objectContaining({docType: 'const', id: 'g'}), + jasmine.objectContaining({docType: 'const', id: 'h'}), + ]); + }); + + it('should grab the content from the first additional declaration if there is no "real" content already', + () => { + const getContent = jasmine.createSpy('getContent').and.returnValue('additional content'); + const additionalDeclaration1 = {}; + const additionalDeclaration2 = {}; + const additionalDeclaration3 = {}; + const processor = processorFactory({getContent}); + const docs = [ + { + docType: 'interface', + id: 'a', + content: 'original content', + additionalDeclarations: [additionalDeclaration1] + }, + { + docType: 'interface', + id: 'b', + content: '@publicApi', // this does not count as "real" content + additionalDeclarations: [additionalDeclaration2] + }, + {docType: 'interface', id: 'c', additionalDeclarations: [additionalDeclaration3]}, + ]; + processor.$process(docs); + expect(docs[0].content).toEqual('original content'); + expect(docs[1].content).toEqual('additional content'); + expect(docs[2].content).toEqual('additional content'); + expect(getContent).toHaveBeenCalledWith(additionalDeclaration1); + expect(getContent).toHaveBeenCalledWith(additionalDeclaration2); + expect(getContent).toHaveBeenCalledWith(additionalDeclaration3); + }); + + it('should extract any __new member from the interface members', () => { + const getContent = jasmine.createSpy('getContent').and.returnValue('additional content'); + const processor = processorFactory({getContent}); + const docs = [ + {docType: 'interface', id: 'a', additionalDeclarations: [{}]}, + {docType: 'interface', id: 'b', additionalDeclarations: [{}], members: []}, + {docType: 'interface', id: 'c', additionalDeclarations: [{}], members: [{name: 'member1'}]}, + { + docType: 'interface', + id: 'd', + additionalDeclarations: [{}], + members: [{name: 'member1', isNewMember: true}] + }, + ]; + processor.$process(docs); + + expect(docs[0].members).toEqual(undefined); + expect(docs[1].members).toEqual([]); + expect(docs[2].members).toEqual([{name: 'member1'}]); + + expect(docs[3].members).toEqual([]); + expect(docs[3].constructorDoc).toEqual({name: 'constructor', isNewMember: true}); + }); +});