Skip to content

Commit

Permalink
feat(compiler): extract docs via exports (#51828)
Browse files Browse the repository at this point in the history
So far this docs extraction has pulls API info from all exported symbols in the program. This commit changes to extracting only symbols that are exported via a specified entry-point. This commit also exports the docs entities through the compiler-cli `index.ts`.

PR Close #51828
  • Loading branch information
jelbourn authored and pkozlowski-opensource committed Sep 20, 2023
1 parent 2c09d51 commit 34495b3
Show file tree
Hide file tree
Showing 15 changed files with 237 additions and 112 deletions.
3 changes: 3 additions & 0 deletions packages/compiler-cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ export {OptimizeFor} from './src/ngtsc/typecheck/api';
export {ConsoleLogger, Logger, LogLevel} from './src/ngtsc/logging';
export {NodeJSFileSystem} from './src/ngtsc/file_system';

// Export documentation entities for Angular-internal API doc generation.
export * from './src/ngtsc/docs/src/entities';

setFileSystem(new NodeJSFileSystem());
20 changes: 13 additions & 7 deletions packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,20 +661,26 @@ export class NgCompiler {
* Gets information for the current program that may be used to generate API
* reference documentation. This includes Angular-specific information, such
* as component inputs and outputs.
*
* @param entryPoint Path to the entry point for the package for which API
* docs should be extracted.
*/
getApiDocumentation(): DocEntry[] {
getApiDocumentation(entryPoint: string): DocEntry[] {
const compilation = this.ensureAnalyzed();
const checker = this.inputProgram.getTypeChecker();
const docsExtractor = new DocsExtractor(checker, compilation.metaReader);

let entries: DocEntry[] = [];
for (const sourceFile of this.inputProgram.getSourceFiles()) {
// We don't want to generate docs for `.d.ts` files.
if (sourceFile.isDeclarationFile) continue;
const entryPointSourceFile = this.inputProgram.getSourceFiles().find(sourceFile => {
// TODO: this will need to be more specific than `.includes`, but the exact path comparison
// will be easier to figure out when the pipeline is running end-to-end.
return sourceFile.fileName.includes(entryPoint);
});

entries.push(...docsExtractor.extractAll(sourceFile));
if (!entryPointSourceFile) {
throw new Error(`Entry point "${entryPoint}" not found in program sources.`);
}
return entries;

return docsExtractor.extractAll(entryPointSourceFile);
}

/**
Expand Down
57 changes: 34 additions & 23 deletions packages/compiler-cli/src/ngtsc/docs/src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {FunctionExtractor} from '@angular/compiler-cli/src/ngtsc/docs/src/functi
import ts from 'typescript';

import {MetadataReader} from '../../metadata';
import {isNamedClassDeclaration} from '../../reflection';
import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';

import {extractClass} from './class_extractor';
import {extractConstant, isSyntheticAngularConstant} from './constant_extractor';
Expand All @@ -26,44 +26,55 @@ export class DocsExtractor {
constructor(private typeChecker: ts.TypeChecker, private metadataReader: MetadataReader) {}

/**
* Gets the set of all documentable entries from a source file.
* Gets the set of all documentable entries from a source file, including
* declarations that are re-exported from this file as an entry-point.
*
* @param sourceFile The file from which to extract documentable entries.
*/
extractAll(sourceFile: ts.SourceFile): DocEntry[] {
const entries: DocEntry[] = [];

for (const statement of sourceFile.statements) {
if (!this.isExported(statement)) continue;
// Use the reflection host to get all the exported declarations from this
// source file entry point.
const reflector = new TypeScriptReflectionHost(this.typeChecker);
const exportedDeclarationMap = reflector.getExportsOfModule(sourceFile);

// Sort the declaration nodes into declaration position because their order is lost in
// reading from the export map. This is primarily useful for testing and debugging.
const exportedDeclarations =
Array.from(exportedDeclarationMap?.entries() ?? [])
.map(([exportName, declaration]) => [exportName, declaration.node] as const)
.sort(([a, declarationA], [b, declarationB]) => declarationA.pos - declarationB.pos);

for (const [exportName, node] of exportedDeclarations) {
let entry: DocEntry|undefined = undefined;

// Ignore anonymous classes.
if (isNamedClassDeclaration(statement)) {
entries.push(extractClass(statement, this.metadataReader, this.typeChecker));
if (isNamedClassDeclaration(node)) {
entry = extractClass(node, this.metadataReader, this.typeChecker);
}

if (ts.isFunctionDeclaration(node)) {
const functionExtractor = new FunctionExtractor(node, this.typeChecker);
entry = functionExtractor.extract();
}

if (ts.isFunctionDeclaration(statement)) {
const functionExtractor = new FunctionExtractor(statement, this.typeChecker);
entries.push(functionExtractor.extract());
if (ts.isVariableDeclaration(node) && !isSyntheticAngularConstant(node)) {
entry = extractConstant(node, this.typeChecker);
}

if (ts.isVariableStatement(statement)) {
statement.declarationList.forEachChild(declaration => {
if (ts.isVariableDeclaration(declaration) && !isSyntheticAngularConstant(declaration)) {
entries.push(extractConstant(declaration, this.typeChecker));
}
});
if (ts.isEnumDeclaration(node)) {
entry = extractEnum(node, this.typeChecker);
}

if (ts.isEnumDeclaration(statement)) {
entries.push(extractEnum(statement, this.typeChecker));
// The exported name of an API may be different from its declaration name, so
// use the declaration name.
if (entry) {
entry.name = exportName;
entries.push(entry);
}
}

return entries;
}

/** Gets whether the given AST node has an `export` modifier. */
private isExported(node: ts.Node): boolean {
return ts.canHaveModifiers(node) &&
(ts.getModifiers(node) ?? []).some(mod => mod.kind === ts.SyntaxKind.ExportKeyword);
}
}
7 changes: 5 additions & 2 deletions packages/compiler-cli/src/ngtsc/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,9 +352,12 @@ export class NgtscProgram implements api.Program {
* Gets information for the current program that may be used to generate API
* reference documentation. This includes Angular-specific information, such
* as component inputs and outputs.
*
* @param entryPoint Path to the entry point for the package for which API
* docs should be extracted.
*/
getApiDocumentation(): DocEntry[] {
return this.compiler.getApiDocumentation();
getApiDocumentation(entryPoint: string): DocEntry[] {
return this.compiler.getApiDocumentation(entryPoint);
}

getEmittedSourceFiles(): Map<string, ts.SourceFile> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ runInEachFileSystem(os => {
});

it('should extract classes', () => {
env.write('test.ts', `
env.write('index.ts', `
export class UserProfile {}
export class CustomSlider {}
`);

const docs: DocEntry[] = env.driveDocsExtraction();
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
expect(docs.length).toBe(2);
expect(docs[0].name).toBe('UserProfile');
expect(docs[0].entryType).toBe(EntryType.UndecoratedClass);
Expand All @@ -40,14 +40,14 @@ runInEachFileSystem(os => {
});

it('should extract class members', () => {
env.write('test.ts', `
env.write('index.ts', `
export class UserProfile {
firstName(): string { return 'Morgan'; }
age: number = 25;
}
`);

const docs: DocEntry[] = env.driveDocsExtraction();
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
const classEntry = docs[0] as ClassEntry;
expect(classEntry.members.length).toBe(2);

Expand All @@ -63,15 +63,15 @@ runInEachFileSystem(os => {
});

it('should extract a method with a rest parameter', () => {
env.write('test.ts', `
env.write('index.ts', `
export class UserProfile {
getNames(prefix: string, ...ids: string[]): string[] {
return [];
}
}
`);

const docs: DocEntry[] = env.driveDocsExtraction();
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
const classEntry = docs[0] as ClassEntry;
const methodEntry = classEntry.members[0] as MethodEntry;
const [prefixParamEntry, idsParamEntry, ] = methodEntry.params;
Expand All @@ -86,13 +86,13 @@ runInEachFileSystem(os => {
});

it('should extract class method params', () => {
env.write('test.ts', `
env.write('index.ts', `
export class UserProfile {
setPhone(num: string, intl: number = 1, area?: string): void {}
}
`);

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

const classEntry = docs[0] as ClassEntry;
expect(classEntry.members.length).toBe(1);
Expand All @@ -117,23 +117,23 @@ runInEachFileSystem(os => {
});

it('should not extract private class members', () => {
env.write('test.ts', `
env.write('index.ts', `
export class UserProfile {
private ssn: string;
private getSsn(): string { return ''; }
private static printSsn(): void { }
}
`);

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

const classEntry = docs[0] as ClassEntry;
expect(classEntry.members.length).toBe(0);
});

it('should extract member tags', () => {
// Test both properties and methods with zero, one, and multiple tags.
env.write('test.ts', `
env.write('index.ts', `
export class UserProfile {
eyeColor = 'brown';
protected name: string;
Expand All @@ -150,7 +150,7 @@ runInEachFileSystem(os => {
}
`);

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

const classEntry = docs[0] as ClassEntry;
expect(classEntry.members.length).toBe(11);
Expand Down Expand Up @@ -189,7 +189,7 @@ runInEachFileSystem(os => {

it('should extract getters and setters', () => {
// Test getter-only, a getter + setter, and setter-only.
env.write('test.ts', `
env.write('index.ts', `
export class UserProfile {
get userId(): number { return 123; }
Expand All @@ -200,7 +200,7 @@ runInEachFileSystem(os => {
}
`);

const docs: DocEntry[] = env.driveDocsExtraction();
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
const classEntry = docs[0] as ClassEntry;
expect(classEntry.entryType).toBe(EntryType.UndecoratedClass);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs';
import {ClassEntry, EntryType, MemberTags, MemberType, MethodEntry, 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';

Expand All @@ -25,13 +24,13 @@ runInEachFileSystem(os => {
});

it('should not extract unexported statements', () => {
env.write('test.ts', `
env.write('index.ts', `
class UserProfile {}
function getUser() { }
const name = '';
`);

const docs: DocEntry[] = env.driveDocsExtraction();
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
expect(docs.length).toBe(0);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ runInEachFileSystem(os => {
});

it('should extract constants', () => {
env.write('test.ts', `
env.write('index.ts', `
export const VERSION = '16.0.0';
`);

const docs: DocEntry[] = env.driveDocsExtraction();
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
expect(docs.length).toBe(1);

const constantEntry = docs[0] as ConstantEntry;
Expand All @@ -39,11 +39,11 @@ runInEachFileSystem(os => {
});

it('should extract multiple constant declarations in a single statement', () => {
env.write('test.ts', `
env.write('index.ts', `
export const PI = 3.14, VERSION = '16.0.0';
`);

const docs: DocEntry[] = env.driveDocsExtraction();
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
expect(docs.length).toBe(2);

const [pi, version] = docs as ConstantEntry[];
Expand All @@ -58,13 +58,13 @@ runInEachFileSystem(os => {
});

it('should extract non-primitive constants', () => {
env.write('test.ts', `
env.write('index.ts', `
import {InjectionToken} from '@angular/core';
export const SOME_TOKEN = new InjectionToken('something');
export const TYPED_TOKEN = new InjectionToken<string>();
`);

const docs: DocEntry[] = env.driveDocsExtraction();
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
expect(docs.length).toBe(2);

const [someToken, typedToken] = docs as ConstantEntry[];
Expand Down
Loading

0 comments on commit 34495b3

Please sign in to comment.