mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(compiler): extract decorator API docs (#52389)
This commit adds decorators to the extracted API docs. It makes some very hard-coded assumptions about the pattern used to declare decorators that's extremely specific to what the framework does today. PR Close #52389
This commit is contained in:
committed by
Alex Rickabaugh
parent
edea54714d
commit
740d46f93b
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @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 {extractInterface} from './class_extractor';
|
||||
import {DecoratorEntry, DecoratorType, EntryType, PropertyEntry} from './entities';
|
||||
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor';
|
||||
|
||||
/** Extracts an API documentation entry for an Angular decorator. */
|
||||
export function extractorDecorator(
|
||||
declaration: ts.VariableDeclaration, typeChecker: ts.TypeChecker): DecoratorEntry {
|
||||
const documentedNode = getDecoratorJsDocNode(declaration);
|
||||
|
||||
const decoratorType = getDecoratorType(declaration);
|
||||
if (!decoratorType) {
|
||||
throw new Error(`"${declaration.name.getText()} is not a decorator."`);
|
||||
}
|
||||
|
||||
return {
|
||||
name: declaration.name.getText(),
|
||||
decoratorType: decoratorType,
|
||||
entryType: EntryType.Decorator,
|
||||
rawComment: extractRawJsDoc(documentedNode),
|
||||
description: extractJsDocDescription(documentedNode),
|
||||
jsdocTags: extractJsDocTags(documentedNode),
|
||||
options: getDecoratorOptions(declaration, typeChecker),
|
||||
};
|
||||
}
|
||||
|
||||
/** Gets whether the given variable declaration is an Angular decorator declaration. */
|
||||
export function isDecoratorDeclaration(declaration: ts.VariableDeclaration): boolean {
|
||||
return !!getDecoratorType(declaration);
|
||||
}
|
||||
|
||||
/** Gets whether an interface is the options interface for a decorator in the same file. */
|
||||
export function isDecoratorOptionsInterface(declaration: ts.InterfaceDeclaration): boolean {
|
||||
return declaration.getSourceFile().statements.some(
|
||||
s => ts.isVariableStatement(s) &&
|
||||
s.declarationList.declarations.some(
|
||||
d => isDecoratorDeclaration(d) && d.name.getText() === declaration.name.getText()));
|
||||
}
|
||||
|
||||
/** Gets the type of decorator, or undefined if the declaration is not a decorator. */
|
||||
function getDecoratorType(declaration: ts.VariableDeclaration): DecoratorType|undefined {
|
||||
// All Angular decorators are initialized with one of `makeDecorator`, `makePropDecorator`,
|
||||
// or `makeParamDecorator`.
|
||||
const initializer = declaration.initializer?.getFullText() ?? '';
|
||||
if (initializer.includes('makeDecorator')) return DecoratorType.Class;
|
||||
if (initializer.includes('makePropDecorator')) return DecoratorType.Member;
|
||||
if (initializer.includes('makeParamDecorator')) return DecoratorType.Parameter;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Gets the doc entry for the options object for an Angular decorator */
|
||||
function getDecoratorOptions(
|
||||
declaration: ts.VariableDeclaration, typeChecker: ts.TypeChecker): PropertyEntry[] {
|
||||
const name = declaration.name.getText();
|
||||
|
||||
// Every decorator has an interface with its options in the same SourceFile.
|
||||
// Queries, however, are defined as a type alias pointing to an interface.
|
||||
const optionsDeclaration = declaration.getSourceFile().statements.find(node => {
|
||||
return (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) &&
|
||||
node.name.getText() === name;
|
||||
});
|
||||
|
||||
if (!optionsDeclaration) {
|
||||
throw new Error(`Decorator "${name}" has no corresponding options interface.`);
|
||||
}
|
||||
|
||||
let optionsInterface: ts.InterfaceDeclaration;
|
||||
if (ts.isTypeAliasDeclaration(optionsDeclaration)) {
|
||||
// We hard-code the assumption that if the decorator's option type is a type alias,
|
||||
// it resolves to a single interface (this is true for all query decorators at time of
|
||||
// this writing).
|
||||
const aliasedType = typeChecker.getTypeAtLocation((optionsDeclaration.type));
|
||||
optionsInterface = (aliasedType.getSymbol()?.getDeclarations() ??
|
||||
[]).find(d => ts.isInterfaceDeclaration(d)) as ts.InterfaceDeclaration;
|
||||
} else {
|
||||
optionsInterface = optionsDeclaration as ts.InterfaceDeclaration;
|
||||
}
|
||||
|
||||
if (!optionsInterface || !ts.isInterfaceDeclaration(optionsInterface)) {
|
||||
throw new Error(`Options for decorator "${name}" is not an interface.`);
|
||||
}
|
||||
|
||||
// Take advantage of the interface extractor to pull the appropriate member info.
|
||||
// Hard code the knowledge that decorator options only have properties, never methods.
|
||||
return extractInterface(optionsInterface, typeChecker).members as PropertyEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the call signature node that has the decorator's public JsDoc block.
|
||||
*
|
||||
* Every decorator has three parts:
|
||||
* - A const that has the actual decorator.
|
||||
* - An interface with the same name as the const that documents the decorator's options.
|
||||
* - An interface suffixed with "Decorator" that has the decorator's call signature and JsDoc block.
|
||||
*
|
||||
* For the description and JsDoc tags, we need the interface suffixed with "Decorator".
|
||||
*/
|
||||
function getDecoratorJsDocNode(declaration: ts.VariableDeclaration): ts.HasJSDoc {
|
||||
const name = declaration.name.getText();
|
||||
|
||||
// Assume the existence of an interface in the same file with the same name
|
||||
// suffixed with "Decorator".
|
||||
const decoratorInterface = declaration.getSourceFile().statements.find(s => {
|
||||
return ts.isInterfaceDeclaration(s) && s.name.getText() === `${name}Decorator`;
|
||||
});
|
||||
|
||||
if (!decoratorInterface || !ts.isInterfaceDeclaration(decoratorInterface)) {
|
||||
throw new Error(`No interface "${name}Decorator" found.`);
|
||||
}
|
||||
|
||||
// The public-facing JsDoc for each decorator is on one of its interface's call signatures.
|
||||
const callSignature = decoratorInterface.members.find(node => {
|
||||
// The description block lives on one of the call signatures for this interface.
|
||||
return ts.isCallSignatureDeclaration(node) && extractRawJsDoc(node);
|
||||
});
|
||||
|
||||
if (!callSignature || !ts.isCallSignatureDeclaration(callSignature)) {
|
||||
throw new Error(`No call signature with JsDoc on "${name}Decorator"`);
|
||||
}
|
||||
|
||||
return callSignature;
|
||||
}
|
||||
@@ -32,6 +32,12 @@ export enum MemberType {
|
||||
EnumItem = 'enum_item',
|
||||
}
|
||||
|
||||
export enum DecoratorType {
|
||||
Class = 'class',
|
||||
Member = 'member',
|
||||
Parameter = 'parameter',
|
||||
}
|
||||
|
||||
/** Informational tags applicable to class members. */
|
||||
export enum MemberTags {
|
||||
Abstract = 'abstract',
|
||||
@@ -90,6 +96,12 @@ export interface EnumEntry extends DocEntry {
|
||||
members: EnumMemberEntry[];
|
||||
}
|
||||
|
||||
/** Documentation entity for an Angular decorator. */
|
||||
export interface DecoratorEntry extends DocEntry {
|
||||
decoratorType: DecoratorType;
|
||||
options: PropertyEntry[];
|
||||
}
|
||||
|
||||
/** Documentation entity for an Angular directives and components. */
|
||||
export interface DirectiveEntry extends ClassEntry {
|
||||
selector: string;
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
* 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';
|
||||
|
||||
import {MetadataReader} from '../../metadata';
|
||||
@@ -15,8 +13,11 @@ import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflectio
|
||||
|
||||
import {extractClass, extractInterface} from './class_extractor';
|
||||
import {extractConstant, isSyntheticAngularConstant} from './constant_extractor';
|
||||
import {extractorDecorator, isDecoratorDeclaration, isDecoratorOptionsInterface} from './decorator_extractor';
|
||||
import {DocEntry} from './entities';
|
||||
import {extractEnum} from './enum_extractor';
|
||||
import {isAngularPrivateName} from './filters';
|
||||
import {FunctionExtractor} from './function_extractor';
|
||||
import {extractTypeAlias} from './type_alias_extractor';
|
||||
|
||||
type DeclarationWithExportName = readonly[string, ts.Declaration];
|
||||
@@ -60,7 +61,7 @@ export class DocsExtractor {
|
||||
return extractClass(node, this.metadataReader, this.typeChecker);
|
||||
}
|
||||
|
||||
if (ts.isInterfaceDeclaration(node)) {
|
||||
if (ts.isInterfaceDeclaration(node) && !isIgnoredInterface(node)) {
|
||||
return extractInterface(node, this.typeChecker);
|
||||
}
|
||||
|
||||
@@ -70,7 +71,8 @@ export class DocsExtractor {
|
||||
}
|
||||
|
||||
if (ts.isVariableDeclaration(node) && !isSyntheticAngularConstant(node)) {
|
||||
return extractConstant(node, this.typeChecker);
|
||||
return isDecoratorDeclaration(node) ? extractorDecorator(node, this.typeChecker) :
|
||||
extractConstant(node, this.typeChecker);
|
||||
}
|
||||
|
||||
if (ts.isTypeAliasDeclaration(node)) {
|
||||
@@ -118,3 +120,13 @@ export class DocsExtractor {
|
||||
([a, declarationA], [b, declarationB]) => declarationA.pos - declarationB.pos);
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets whether an interface should be ignored for docs extraction. */
|
||||
function isIgnoredInterface(node: ts.InterfaceDeclaration) {
|
||||
// We filter out all interfaces that end with "Decorator" because we capture their
|
||||
// types as part of the main decorator entry (which are declared as constants).
|
||||
// This approach to dealing with decorators is admittedly fuzzy, but this aspect of
|
||||
// the framework's source code is unlikely to change. We also filter out the interfaces
|
||||
// that contain the decorator options.
|
||||
return node.name.getText().endsWith('Decorator') || isDecoratorOptionsInterface(node);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* @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 {DecoratorEntry, DecoratorType, EntryType} 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(() => {
|
||||
let env!: NgtscTestEnvironment;
|
||||
|
||||
describe('ngtsc decorator docs extraction', () => {
|
||||
beforeEach(() => {
|
||||
env = NgtscTestEnvironment.setup(testFiles);
|
||||
env.tsconfig();
|
||||
});
|
||||
|
||||
it('should extract class decorators that define options in an interface', () => {
|
||||
env.write('index.ts', `
|
||||
export interface Component {
|
||||
/** The template. */
|
||||
template: string;
|
||||
}
|
||||
|
||||
export interface ComponentDecorator {
|
||||
/** The description. */
|
||||
(obj?: Component): any;
|
||||
}
|
||||
|
||||
function makeDecorator(): ComponentDecorator { return () => {}; }
|
||||
|
||||
export const Component: ComponentDecorator = makeDecorator();
|
||||
`);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const decoratorEntry = docs[0] as DecoratorEntry;
|
||||
expect(decoratorEntry.name).toBe('Component');
|
||||
expect(decoratorEntry.description).toBe('The description.');
|
||||
expect(decoratorEntry.entryType).toBe(EntryType.Decorator);
|
||||
expect(decoratorEntry.decoratorType).toBe(DecoratorType.Class);
|
||||
|
||||
expect(decoratorEntry.options.length).toBe(1);
|
||||
expect(decoratorEntry.options[0].name).toBe('template');
|
||||
expect(decoratorEntry.options[0].type).toBe('string');
|
||||
expect(decoratorEntry.options[0].description).toBe('The template.');
|
||||
});
|
||||
|
||||
it('should extract property decorators', () => {
|
||||
env.write('index.ts', `
|
||||
export interface Input {
|
||||
/** The alias. */
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export interface InputDecorator {
|
||||
/** The description. */
|
||||
(alias: string): any;
|
||||
}
|
||||
|
||||
function makePropDecorator(): InputDecorator { return () => {}); }
|
||||
|
||||
export const Input: InputDecorator = makePropDecorator();
|
||||
`);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const decoratorEntry = docs[0] as DecoratorEntry;
|
||||
expect(decoratorEntry.name).toBe('Input');
|
||||
expect(decoratorEntry.description).toBe('The description.');
|
||||
expect(decoratorEntry.entryType).toBe(EntryType.Decorator);
|
||||
expect(decoratorEntry.decoratorType).toBe(DecoratorType.Member);
|
||||
|
||||
expect(decoratorEntry.options.length).toBe(1);
|
||||
expect(decoratorEntry.options[0].name).toBe('alias');
|
||||
expect(decoratorEntry.options[0].type).toBe('string');
|
||||
expect(decoratorEntry.options[0].description).toBe('The alias.');
|
||||
});
|
||||
|
||||
it('should extract property decorators with a type alias', () => {
|
||||
env.write('index.ts', `
|
||||
interface Query {
|
||||
/** The read. */
|
||||
read: string;
|
||||
}
|
||||
|
||||
export type ViewChild = Query;
|
||||
|
||||
export interface ViewChildDecorator {
|
||||
/** The description. */
|
||||
(alias: string): any;
|
||||
}
|
||||
|
||||
function makePropDecorator(): ViewChildDecorator { return () => {}); }
|
||||
|
||||
export const ViewChild: ViewChildDecorator = makePropDecorator();
|
||||
`);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const decoratorEntry = docs[0] as DecoratorEntry;
|
||||
expect(decoratorEntry.name).toBe('ViewChild');
|
||||
expect(decoratorEntry.description).toBe('The description.');
|
||||
expect(decoratorEntry.entryType).toBe(EntryType.Decorator);
|
||||
expect(decoratorEntry.decoratorType).toBe(DecoratorType.Member);
|
||||
|
||||
expect(decoratorEntry.options.length).toBe(1);
|
||||
expect(decoratorEntry.options[0].name).toBe('read');
|
||||
expect(decoratorEntry.options[0].type).toBe('string');
|
||||
expect(decoratorEntry.options[0].description).toBe('The read.');
|
||||
});
|
||||
|
||||
it('should extract param decorators', () => {
|
||||
env.write('index.ts', `
|
||||
export interface Inject {
|
||||
/** The token. */
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface InjectDecorator {
|
||||
/** The description. */
|
||||
(token: string) => any;
|
||||
}
|
||||
|
||||
function makePropDecorator(): InjectDecorator { return () => {}; }
|
||||
|
||||
export const Inject: InjectDecorator = makeParamDecorator();
|
||||
`);
|
||||
|
||||
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
|
||||
expect(docs.length).toBe(1);
|
||||
|
||||
const decoratorEntry = docs[0] as DecoratorEntry;
|
||||
expect(decoratorEntry.name).toBe('Inject');
|
||||
expect(decoratorEntry.description).toBe('The description.');
|
||||
expect(decoratorEntry.entryType).toBe(EntryType.Decorator);
|
||||
expect(decoratorEntry.decoratorType).toBe(DecoratorType.Parameter);
|
||||
|
||||
expect(decoratorEntry.options.length).toBe(1);
|
||||
expect(decoratorEntry.options[0].name).toBe('token');
|
||||
expect(decoratorEntry.options[0].type).toBe('string');
|
||||
expect(decoratorEntry.options[0].description).toBe('The token.');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user