Skip to content

Commit

Permalink
feat(compiler): extract docs info for enums, pipes, and NgModules (#5…
Browse files Browse the repository at this point in the history
…1733)

Based on top of #51717

This commit adds extraction for enums, pipes, and NgModules. It also adds a couple of tests for JsDoc extraction that weren't covered in the previous commit.

PR Close #51733
  • Loading branch information
jelbourn authored and pkozlowski-opensource committed Sep 18, 2023
1 parent e0b1bb3 commit 2e41488
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 10 deletions.
70 changes: 62 additions & 8 deletions packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts
Expand Up @@ -11,10 +11,10 @@ import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from '@angul
import ts from 'typescript';

import {Reference} from '../../imports';
import {DirectiveMeta, InputMapping, InputOrOutput, MetadataReader} from '../../metadata';
import {DirectiveMeta, InputMapping, InputOrOutput, MetadataReader, NgModuleMeta, PipeMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';

import {ClassEntry, DirectiveEntry, EntryType, MemberEntry, MemberTags, MemberType, MethodEntry, PropertyEntry} from './entities';
import {ClassEntry, DirectiveEntry, EntryType, MemberEntry, MemberTags, MemberType, MethodEntry, PipeEntry, PropertyEntry} from './entities';
import {extractResolvedTypeString} from './type_extractor';

/** A class member declaration that is *like* a property (including accessors) */
Expand Down Expand Up @@ -211,15 +211,69 @@ class DirectiveExtractor extends ClassExtractor {
}
}

/** Extractor to pull info for API reference documentation for an Angular pipe. */
class PipeExtractor extends ClassExtractor {
constructor(
declaration: ClassDeclaration&ts.ClassDeclaration,
reference: Reference,
private metadata: PipeMeta,
typeChecker: ts.TypeChecker,
) {
super(declaration, reference, typeChecker);
}

override extract(): PipeEntry {
return {
...super.extract(),
pipeName: this.metadata.name,
entryType: EntryType.Pipe,
isStandalone: this.metadata.isStandalone,
};
}
}

/** Extractor to pull info for API reference documentation for an Angular pipe. */
class NgModuleExtractor extends ClassExtractor {
constructor(
declaration: ClassDeclaration&ts.ClassDeclaration,
reference: Reference,
private metadata: NgModuleMeta,
typeChecker: ts.TypeChecker,
) {
super(declaration, reference, typeChecker);
}

override extract(): ClassEntry {
return {
...super.extract(),
entryType: EntryType.NgModule,
};
}
}

/** Extracts documentation info for a class, potentially including Angular-specific info. */
export function extractClass(
classDeclaration: ClassDeclaration&ts.ClassDeclaration, metadataReader: MetadataReader,
typeChecker: ts.TypeChecker): ClassEntry {
classDeclaration: ClassDeclaration&ts.ClassDeclaration,
metadataReader: MetadataReader,
typeChecker: ts.TypeChecker,
): ClassEntry {
const ref = new Reference(classDeclaration);
const metadata = metadataReader.getDirectiveMetadata(ref);
const extractor = metadata ?
new DirectiveExtractor(classDeclaration, ref, metadata, typeChecker) :
new ClassExtractor(classDeclaration, ref, typeChecker);

let extractor: ClassExtractor;

let directiveMetadata = metadataReader.getDirectiveMetadata(ref);
let pipeMetadata = metadataReader.getPipeMetadata(ref);
let ngModuleMetadata = metadataReader.getNgModuleMetadata(ref);

if (directiveMetadata) {
extractor = new DirectiveExtractor(classDeclaration, ref, directiveMetadata, typeChecker);
} else if (pipeMetadata) {
extractor = new PipeExtractor(classDeclaration, ref, pipeMetadata, typeChecker);
} else if (ngModuleMetadata) {
extractor = new NgModuleExtractor(classDeclaration, ref, ngModuleMetadata, typeChecker);
} else {
extractor = new ClassExtractor(classDeclaration, ref, typeChecker);
}

return extractor.extract();
}
21 changes: 20 additions & 1 deletion packages/compiler-cli/src/ngtsc/docs/src/entities.ts
Expand Up @@ -17,6 +17,7 @@ export enum EntryType {
Enum = 'enum',
Function = 'function',
Interface = 'interface',
NgModule = 'ng_module',
Pipe = 'pipe',
TypeAlias = 'type_alias',
UndecoratedClass = 'undecorated_class',
Expand All @@ -28,6 +29,7 @@ export enum MemberType {
Method = 'method',
Getter = 'getter',
Setter = 'setter',
EnumItem = 'enum_item',
}

/** Informational tags applicable to class members. */
Expand Down Expand Up @@ -64,19 +66,30 @@ export interface ClassEntry extends DocEntry {
members: MemberEntry[];
}

/** Documentation entity for a TypeScript enum. */
export interface EnumEntry extends DocEntry {
members: EnumMemberEntry[];
}

/** Documentation entity for an Angular directives and components. */
export interface DirectiveEntry extends ClassEntry {
selector: string;
exportAs: string[];
isStandalone: boolean;
}

export interface PipeEntry extends ClassEntry {
pipeName: string;
isStandalone: boolean;
// TODO: add `isPure`.
}

export interface FunctionEntry extends DocEntry {
params: ParameterEntry[];
returnType: string;
}

/** Sub-entry for a single class member. */
/** Sub-entry for a single class or enum member. */
export interface MemberEntry {
name: string;
memberType: MemberType;
Expand All @@ -85,6 +98,12 @@ export interface MemberEntry {
jsdocTags: JsDocTagEntry[];
}

/** Sub-entry for an enum member. */
export interface EnumMemberEntry extends MemberEntry {
type: string;
value: string;
}

/** Sub-entry for a class property. */
export interface PropertyEntry extends MemberEntry {
type: string;
Expand Down
49 changes: 49 additions & 0 deletions packages/compiler-cli/src/ngtsc/docs/src/enum_extractor.ts
@@ -0,0 +1,49 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {EntryType, EnumEntry, EnumMemberEntry, MemberType} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from '@angular/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor';
import {extractResolvedTypeString} from '@angular/compiler-cli/src/ngtsc/docs/src/type_extractor';
import ts from 'typescript';


/** Extracts documentation entry for an enum. */
export function extractEnum(
declaration: ts.EnumDeclaration, typeChecker: ts.TypeChecker): EnumEntry {
return {
name: declaration.name.getText(),
entryType: EntryType.Enum,
members: extractEnumMembers(declaration, typeChecker),
rawComment: extractRawJsDoc(declaration),
description: extractJsDocDescription(declaration),
jsdocTags: extractJsDocTags(declaration),
};
}

/** Extracts doc info for an enum's members. */
function extractEnumMembers(
declaration: ts.EnumDeclaration, checker: ts.TypeChecker): EnumMemberEntry[] {
return declaration.members.map(member => ({
name: member.name.getText(),
type: extractResolvedTypeString(member, checker),
value: getEnumMemberValue(member),
memberType: MemberType.EnumItem,
jsdocTags: extractJsDocTags(member),
description: extractJsDocDescription(member),
memberTags: [],
}));
}

/** Gets the explicitly assigned value for an enum member, or an empty string if there is none. */
function getEnumMemberValue(memberNode: ts.EnumMember): string {
// If the enum member has a child number literal or string literal,
// we use that literal as the "value" of the member.
const literal =
memberNode.getChildren().find(n => ts.isNumericLiteral(n) || ts.isStringLiteral(n));
return literal?.getText() ?? '';
}
5 changes: 5 additions & 0 deletions packages/compiler-cli/src/ngtsc/docs/src/extractor.ts
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {extractEnum} from '@angular/compiler-cli/src/ngtsc/docs/src/enum_extractor';
import {FunctionExtractor} from '@angular/compiler-cli/src/ngtsc/docs/src/function_extractor';
import ts from 'typescript';

Expand Down Expand Up @@ -51,6 +52,10 @@ export class DocsExtractor {
}
});
}

if (ts.isEnumDeclaration(statement)) {
entries.push(extractEnum(statement, this.typeChecker));
}
}

return entries;
Expand Down
@@ -0,0 +1,91 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs';
import {ClassEntry, DirectiveEntry, EntryType, EnumEntry, MemberTags, PropertyEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';

import {NgtscTestEnvironment} from '../env';

const testFiles = loadStandardTestFiles({fakeCore: true, fakeCommon: true});

runInEachFileSystem(os => {
let env!: NgtscTestEnvironment;

describe('ngtsc enum docs extraction', () => {
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig();
});

it('should extract enum info without explicit values', () => {
env.write('test.ts', `
export enum PizzaTopping {
/** It is cheese */
Cheese,
/** Or "tomato" if you are British */
Tomato,
}
`);

const docs: DocEntry[] = env.driveDocsExtraction();

expect(docs.length).toBe(1);
expect(docs[0].entryType).toBe(EntryType.Enum);

const enumEntry = docs[0] as EnumEntry;
expect(enumEntry.name).toBe('PizzaTopping');
expect(enumEntry.members.length).toBe(2);

const [cheeseEntry, tomatoEntry] = enumEntry.members;

expect(cheeseEntry.name).toBe('Cheese');
expect(cheeseEntry.description).toBe('It is cheese');
expect(cheeseEntry.value).toBe('');

expect(tomatoEntry.name).toBe('Tomato');
expect(tomatoEntry.description).toBe('Or "tomato" if you are British');
expect(tomatoEntry.value).toBe('');
});

it('should extract enum info with explicit values', () => {
env.write('test.ts', `
export enum PizzaTopping {
/** It is cheese */
Cheese = 0,
/** Or "tomato" if you are British */
Tomato = "tomato",
}
`);

const docs: DocEntry[] = env.driveDocsExtraction();

expect(docs.length).toBe(1);
expect(docs[0].entryType).toBe(EntryType.Enum);

const enumEntry = docs[0] as EnumEntry;
expect(enumEntry.name).toBe('PizzaTopping');
expect(enumEntry.members.length).toBe(2);

const [cheeseEntry, tomatoEntry] = enumEntry.members;

expect(cheeseEntry.name).toBe('Cheese');
expect(cheeseEntry.description).toBe('It is cheese');
expect(cheeseEntry.value).toBe('0');
expect(cheeseEntry.type).toBe('PizzaTopping.Cheese');

expect(tomatoEntry.name).toBe('Tomato');
expect(tomatoEntry.description).toBe('Or "tomato" if you are British');
expect(tomatoEntry.value).toBe('"tomato"');
expect(tomatoEntry.type).toBe('PizzaTopping.Tomato');
});
});
});
Expand Up @@ -7,7 +7,7 @@
*/

import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs';
import {ConstantEntry, EntryType, FunctionEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
import {ClassEntry, FunctionEntry, MethodEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';

Expand Down Expand Up @@ -248,5 +248,45 @@ runInEachFileSystem(os => {
expect(dataEntry.description).toBe('The data to save.');
expect(timingEntry.description).toBe('Long description\nwith multiple lines.');
});

it('should extract class member descriptions', () => {
env.write('test.ts', `
export class UserProfile {
/** A user identifier. */
userId: number = 0;
/** Name of the user */
get name(): string { return ''; }
/** Name of the user */
set name(value: string) { }
/**
* Save the user.
* @param config Setting for saving.
* @returns Whether it succeeded
*/
save(config: object): boolean { return false; }
}
`);

const docs: DocEntry[] = env.driveDocsExtraction();
expect(docs.length).toBe(1);
const classEntry = docs[0] as ClassEntry;

expect(classEntry.members.length).toBe(4);
const [userIdEntry, nameGetterEntry, nameSetterEntry, ] = classEntry.members;

expect(userIdEntry.description).toBe('A user identifier.');
expect(nameGetterEntry.description).toBe('Name of the user');
expect(nameSetterEntry.description).toBe('Name of the user');

const saveEntry = classEntry.members[3] as MethodEntry;
expect(saveEntry.description).toBe('Save the user.');

expect(saveEntry.params[0].description).toBe('Setting for saving.');
expect(saveEntry.jsdocTags.length).toBe(2);
expect(saveEntry.jsdocTags[1]).toEqual({name: 'returns', comment: 'Whether it succeeded'});
});
});
});

0 comments on commit 2e41488

Please sign in to comment.