Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/compiler-cli/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/core",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/docs",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/indexer",
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/core/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/annotations/common",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/docs",
"//packages/compiler-cli/src/ngtsc/entry_point",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
Expand Down
21 changes: 21 additions & 0 deletions packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecorato
import {InjectableClassRegistry} from '../../annotations/common';
import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../cycles';
import {COMPILER_ERRORS_WITH_GUIDES, ERROR_DETAILS_PAGE_BASE_URL, ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../diagnostics';
import {DocEntry, DocsExtractor} from '../../docs';
import {checkForPrivateExports, ReferenceGraph} from '../../entry_point';
import {absoluteFromSourceFile, AbsoluteFsPath, LogicalFileSystem, resolve} from '../../file_system';
import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, DeferredSymbolTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports';
Expand Down Expand Up @@ -656,6 +657,26 @@ export class NgCompiler {
return generateAnalysis(context);
}

/**
* 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.
*/
getApiDocumentation(): 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;

entries.push(...docsExtractor.extractAll(sourceFile));
}
return entries;
}

/**
* Collect i18n messages into the `Xi18nContext`.
*/
Expand Down
20 changes: 20 additions & 0 deletions packages/compiler-cli/src/ngtsc/docs/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
load("//tools:defaults.bzl", "ts_library")

package(default_visibility = ["//visibility:public"])

# Compiler code pertaining to extracting data for generating API reference documentation.
ts_library(
name = "docs",
srcs = ["index.ts"] + glob([
"src/**/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/docs",
deps = [
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node",
"@npm//typescript",
],
)
10 changes: 10 additions & 0 deletions packages/compiler-cli/src/ngtsc/docs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @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
*/

export {DocEntry} from './src/entities';
export {DocsExtractor} from './src/extractor';
225 changes: 225 additions & 0 deletions packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/**
* @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 {FunctionExtractor} from '@angular/compiler-cli/src/ngtsc/docs/src/function_extractor';
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from '@angular/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor';
import ts from 'typescript';

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

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

/** A class member declaration that is *like* a property (including accessors) */
type PropertyDeclarationLike = ts.PropertyDeclaration|ts.AccessorDeclaration;

/** Extractor to pull info for API reference documentation for a TypeScript class. */
class ClassExtractor {
constructor(
protected declaration: ClassDeclaration&ts.ClassDeclaration,
protected reference: Reference,
protected typeChecker: ts.TypeChecker,
) {}

/** Extract docs info specific to classes. */
extract(): ClassEntry {
return {
name: this.declaration.name!.text,
entryType: EntryType.UndecoratedClass,
members: this.extractAllClassMembers(this.declaration),
description: extractJsDocDescription(this.declaration),
jsdocTags: extractJsDocTags(this.declaration),
rawComment: extractRawJsDoc(this.declaration),
};
}

/** Extracts doc info for a class's members. */
protected extractAllClassMembers(classDeclaration: ts.ClassDeclaration): MemberEntry[] {
const members: MemberEntry[] = [];

for (const member of classDeclaration.members) {
if (this.isMemberExcluded(member)) continue;

const memberEntry = this.extractClassMember(member);
if (memberEntry) {
members.push(memberEntry);
}
}

return members;
}

/** Extract docs for a class's members (methods and properties). */
protected extractClassMember(memberDeclaration: ts.ClassElement): MemberEntry|undefined {
if (ts.isMethodDeclaration(memberDeclaration)) {
return this.extractMethod(memberDeclaration);
} else if (ts.isPropertyDeclaration(memberDeclaration)) {
return this.extractClassProperty(memberDeclaration);
} else if (ts.isAccessor(memberDeclaration)) {
return this.extractGetterSetter(memberDeclaration);
}

// We only expect methods, properties, and accessors. If we encounter something else,
// return undefined and let the rest of the program filter it out.
return undefined;
}

/** Extracts docs for a class method. */
protected extractMethod(methodDeclaration: ts.MethodDeclaration): MethodEntry {
const functionExtractor = new FunctionExtractor(methodDeclaration, this.typeChecker);
return {
...functionExtractor.extract(),
memberType: MemberType.Method,
memberTags: this.getMemberTags(methodDeclaration),
};
}

/** Extracts doc info for a property declaration. */
protected extractClassProperty(propertyDeclaration: PropertyDeclarationLike): PropertyEntry {
return {
name: propertyDeclaration.name.getText(),
type: extractResolvedTypeString(propertyDeclaration, this.typeChecker),
memberType: MemberType.Property,
memberTags: this.getMemberTags(propertyDeclaration),
description: extractJsDocDescription(propertyDeclaration),
jsdocTags: extractJsDocTags(propertyDeclaration),
};
}

/** Extracts doc info for an accessor member (getter/setter). */
protected extractGetterSetter(accessor: ts.AccessorDeclaration): PropertyEntry {
return {
...this.extractClassProperty(accessor),
memberType: ts.isGetAccessor(accessor) ? MemberType.Getter : MemberType.Setter,
};
}

/** Gets the tags for a member (protected, readonly, static, etc.) */
protected getMemberTags(member: ts.MethodDeclaration|ts.PropertyDeclaration|
ts.AccessorDeclaration): MemberTags[] {
const tags: MemberTags[] = this.getMemberTagsFromModifiers(member.modifiers ?? []);

if (member.questionToken) {
tags.push(MemberTags.Optional);
}

return tags;
}

/** Get the tags for a member that come from the declaration modifiers. */
private getMemberTagsFromModifiers(mods: Iterable<ts.ModifierLike>): MemberTags[] {
const tags: MemberTags[] = [];
for (const mod of mods) {
const tag = this.getTagForMemberModifier(mod);
if (tag) tags.push(tag);
}
return tags;
}

/** Gets the doc tag corresponding to a class member modifier (readonly, protected, etc.). */
private getTagForMemberModifier(mod: ts.ModifierLike): MemberTags|undefined {
switch (mod.kind) {
case ts.SyntaxKind.StaticKeyword:
return MemberTags.Static;
case ts.SyntaxKind.ReadonlyKeyword:
return MemberTags.Readonly;
case ts.SyntaxKind.ProtectedKeyword:
return MemberTags.Protected;
default:
return undefined;
}
}

/**
* Gets whether a given class member should be excluded from public API docs.
* This is the case if:
* - The member does not have a name
* - The member is neither a method nor property
* - The member is protected
*/
private isMemberExcluded(member: ts.ClassElement): boolean {
return !member.name || !this.isDocumentableMember(member) ||
!!member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword);
}

/** Gets whether a class member is a method, property, or accessor. */
private isDocumentableMember(member: ts.ClassElement): member is ts.MethodDeclaration
|ts.PropertyDeclaration {
return ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member) ||
ts.isAccessor(member);
}
}

/** Extractor to pull info for API reference documentation for an Angular directive. */
class DirectiveExtractor extends ClassExtractor {
constructor(
declaration: ClassDeclaration&ts.ClassDeclaration,
reference: Reference,
protected metadata: DirectiveMeta,
checker: ts.TypeChecker,
) {
super(declaration, reference, checker);
}

/** Extract docs info for directives and components (including underlying class info). */
override extract(): DirectiveEntry {
return {
...super.extract(),
isStandalone: this.metadata.isStandalone,
selector: this.metadata.selector ?? '',
exportAs: this.metadata.exportAs ?? [],
entryType: this.metadata.isComponent ? EntryType.Component : EntryType.Directive,
};
}

/** Extracts docs info for a directive property, including input/output metadata. */
override extractClassProperty(propertyDeclaration: ts.PropertyDeclaration): PropertyEntry {
const entry = super.extractClassProperty(propertyDeclaration);

const inputMetadata = this.getInputMetadata(propertyDeclaration);
if (inputMetadata) {
entry.memberTags.push(MemberTags.Input);
entry.inputAlias = inputMetadata.bindingPropertyName;
}

const outputMetadata = this.getOutputMetadata(propertyDeclaration);
if (outputMetadata) {
entry.memberTags.push(MemberTags.Output);
entry.outputAlias = outputMetadata.bindingPropertyName;
}

return entry;
}

/** Gets the input metadata for a directive property. */
private getInputMetadata(prop: ts.PropertyDeclaration): InputMapping|undefined {
const propName = prop.name.getText();
return this.metadata.inputs?.getByClassPropertyName(propName) ?? undefined;
}

/** Gets the output metadata for a directive property. */
private getOutputMetadata(prop: ts.PropertyDeclaration): InputOrOutput|undefined {
const propName = prop.name.getText();
return this.metadata?.outputs?.getByClassPropertyName(propName) ?? undefined;
}
}

/** Extracts documentation info for a class, potentially including Angular-specific info. */
export function extractClass(
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);

return extractor.extract();
}
45 changes: 45 additions & 0 deletions packages/compiler-cli/src/ngtsc/docs/src/constant_extractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @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 ts from 'typescript';

import {ConstantEntry, EntryType} from './entities';
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc,} from './jsdoc_extractor';

/** Extracts documentation entry for a constant. */
export function extractConstant(
declaration: ts.VariableDeclaration, typeChecker: ts.TypeChecker): ConstantEntry {
// For constants specifically, we want to get the base type for any literal types.
// For example, TypeScript by default extracts `const PI = 3.14` as PI having a type of the
// literal `3.14`. We don't want this behavior for constants, since generally one wants the
// _value_ of the constant to be able to change between releases without changing the type.
// `VERSION` is a good example here; the version is always a `string`, but the actual value of
// the version string shouldn't matter to the type system.
const resolvedType =
typeChecker.getBaseTypeOfLiteralType(typeChecker.getTypeAtLocation(declaration));

// In the TS AST, the leading comment for a variable declaration is actually
// on the ancestor `ts.VariableStatement` (since a single variable statement may
// contain multiple variable declarations).
const variableStatement = declaration.parent.parent;
const rawComment = extractRawJsDoc(declaration.parent.parent);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a VariableDeclaration outside of a variable list, statement- but would you want to assert this? I think that would make this code more robust and avoid future hard-to-debug scenarios?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type of declaration.parent.parent here is ts.VariableStatement | ts.ForStatement | ts.ForOfStatement | ts.ForInStatement | ts.TryStatement. In the scenario that this code path runs for a for or try statement, I think the current behavior (an empty string if the node doesn't have any JsDoc) is reasonable.

I also expect to need to refine things once I start running it over the real sources.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Just thought might make things easier to debug if we had an actual runtime assert here


return {
name: declaration.name.getText(),
type: typeChecker.typeToString(resolvedType),
entryType: EntryType.Constant,
rawComment,
description: extractJsDocDescription(declaration),
jsdocTags: extractJsDocTags(declaration),
};
}

/** Gets whether a given constant is an Angular-added const that should be ignored for docs. */
export function isSyntheticAngularConstant(declaration: ts.VariableDeclaration) {
return declaration.name.getText() === 'USED_FOR_NG_TYPE_CHECKING';
}
Loading