Skip to content

Commit

Permalink
refactor(compiler): extract generic info for api reference (angular#5…
Browse files Browse the repository at this point in the history
…2204)

This commit extracts the API reference info for generic parameters for
classes, methods, interfaces, and functions. It includes any constraints
and the default type if present.

PR Close angular#52204
  • Loading branch information
jelbourn authored and pkozlowski-opensource committed Oct 17, 2023
1 parent 7466004 commit 634c529
Show file tree
Hide file tree
Showing 17 changed files with 161 additions and 21 deletions.
6 changes: 4 additions & 2 deletions packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
* 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';
Expand All @@ -16,6 +14,9 @@ import {ClassDeclaration} from '../../reflection';

import {ClassEntry, DirectiveEntry, EntryType, InterfaceEntry, MemberEntry, MemberTags, MemberType, MethodEntry, PipeEntry, PropertyEntry} from './entities';
import {isAngularPrivateName} from './filters';
import {FunctionExtractor} from './function_extractor';
import {extractGenerics} from './generics_extractor';
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor';
import {extractResolvedTypeString} from './type_extractor';

// For the purpose of extraction, we can largely treat properties and accessors the same.
Expand Down Expand Up @@ -53,6 +54,7 @@ class ClassExtractor {
entryType: ts.isInterfaceDeclaration(this.declaration) ? EntryType.Interface :
EntryType.UndecoratedClass,
members: this.extractAllClassMembers(this.declaration),
generics: extractGenerics(this.declaration),
description: extractJsDocDescription(this.declaration),
jsdocTags: extractJsDocTags(this.declaration),
rawComment: extractRawJsDoc(this.declaration),
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler-cli/src/ngtsc/docs/src/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,19 @@ export enum MemberTags {
Output = 'output',
}

/** Documentation entity for single JsDoc tag. */
export interface JsDocTagEntry {
name: string;
comment: string;
}

/** Documentation entity for single generic parameter. */
export interface GenericEntry {
name: string;
constraint: string|undefined;
default: string|undefined;
}

/** Base type for all documentation entities. */
export interface DocEntry {
entryType: EntryType;
Expand All @@ -69,6 +77,7 @@ export type TypeAliasEntry = ConstantEntry;
export interface ClassEntry extends DocEntry {
isAbstract: boolean;
members: MemberEntry[];
generics: GenericEntry[];
}

// From an API doc perspective, class and interfaces are identical.
Expand Down Expand Up @@ -97,6 +106,7 @@ export interface PipeEntry extends ClassEntry {
export interface FunctionEntry extends DocEntry {
params: ParameterEntry[];
returnType: string;
generics: GenericEntry[];
}

/** Sub-entry for a single class or enum member. */
Expand Down
13 changes: 7 additions & 6 deletions packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/

import {EntryType, FunctionEntry, ParameterEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from '@angular/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor';
import ts from 'typescript';

import {EntryType, FunctionEntry, ParameterEntry} from './entities';
import {extractGenerics} from './generics_extractor';
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor';
import {extractResolvedTypeString} from './type_extractor';

export type FunctionLike = ts.FunctionDeclaration|ts.MethodDeclaration|ts.MethodSignature;

export class FunctionExtractor {
constructor(
private declaration: ts.FunctionDeclaration|ts.MethodDeclaration|ts.MethodSignature,
private typeChecker: ts.TypeChecker,
) {}
constructor(private declaration: FunctionLike, private typeChecker: ts.TypeChecker) {}

extract(): FunctionEntry {
// TODO: is there any real situation in which the signature would not be available here?
Expand All @@ -33,6 +33,7 @@ export class FunctionExtractor {
name: this.declaration.name!.getText(),
returnType,
entryType: EntryType.Function,
generics: extractGenerics(this.declaration),
description: extractJsDocDescription(this.declaration),
jsdocTags: extractJsDocTags(this.declaration),
rawComment: extractRawJsDoc(this.declaration),
Expand Down
25 changes: 25 additions & 0 deletions packages/compiler-cli/src/ngtsc/docs/src/generics_extractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @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 {GenericEntry} from './entities';

type DeclarationWithTypeParams = {
typeParameters?: ts.NodeArray<ts.TypeParameterDeclaration>|undefined
};

/** Gets a list of all the generic type parameters for a declaration. */
export function extractGenerics(declaration: DeclarationWithTypeParams): GenericEntry[] {
return declaration.typeParameters?.map(typeParam => ({
name: typeParam.name.getText(),
constraint: typeParam.constraint?.getText(),
default: typeParam.default?.getText(),
})) ??
[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc class docs extraction', () => {
Expand Down Expand Up @@ -296,5 +296,90 @@ runInEachFileSystem(os => {
expect(resetEntry.name).toBe('reset');
expect(resetEntry.memberTags).toContain(MemberTags.Abstract);
});

it('should extract class generic parameters', () => {
env.write('index.ts', `
export class UserProfile<T> {
constructor(public name: T) { }
}
export class TwinProfile<U, V> {
constructor(public name: U, age: V) { }
}
export class AdminProfile<X extends String> {
constructor(public name: X) { }
}
export class BotProfile<Q = string> {
constructor(public name: Q) { }
}
export class ExecProfile<W extends String = string> {
constructor(public name: W) { }
}`);

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

const [
userProfileEntry,
twinProfileEntry,
adminProfileEntry,
botProfileEntry,
execProfileEntry,
] = docs as ClassEntry[];

expect(userProfileEntry.generics.length).toBe(1);
expect(twinProfileEntry.generics.length).toBe(2);
expect(adminProfileEntry.generics.length).toBe(1);
expect(botProfileEntry.generics.length).toBe(1);
expect(execProfileEntry.generics.length).toBe(1);

const [userProfileGenericEntry] = userProfileEntry.generics;
expect(userProfileGenericEntry.name).toBe('T');
expect(userProfileGenericEntry.constraint).toBeUndefined();
expect(userProfileGenericEntry.default).toBeUndefined();

const [nameGenericEntry, ageGenericEntry] = twinProfileEntry.generics;
expect(nameGenericEntry.name).toBe('U');
expect(nameGenericEntry.constraint).toBeUndefined();
expect(nameGenericEntry.default).toBeUndefined();
expect(ageGenericEntry.name).toBe('V');
expect(ageGenericEntry.constraint).toBeUndefined();
expect(ageGenericEntry.default).toBeUndefined();

const [adminProfileGenericEntry] = adminProfileEntry.generics;
expect(adminProfileGenericEntry.name).toBe('X');
expect(adminProfileGenericEntry.constraint).toBe('String');
expect(adminProfileGenericEntry.default).toBeUndefined();

const [botProfileGenericEntry] = botProfileEntry.generics;
expect(botProfileGenericEntry.name).toBe('Q');
expect(botProfileGenericEntry.constraint).toBeUndefined();
expect(botProfileGenericEntry.default).toBe('string');

const [execProfileGenericEntry] = execProfileEntry.generics;
expect(execProfileGenericEntry.name).toBe('W');
expect(execProfileGenericEntry.constraint).toBe('String');
expect(execProfileGenericEntry.default).toBe('string');
});

it('should extract method generic parameters', () => {
env.write('index.ts', `
export class UserProfile {
save<T>(data: T): void { }
}`);

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

const classEntry = docs[0] as ClassEntry;
const [genericEntry] = (classEntry.members[0] as MethodEntry).generics;

expect(genericEntry.name).toBe('T');
expect(genericEntry.constraint).toBeUndefined();
expect(genericEntry.default).toBeUndefined();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc common docs extraction', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc constant docs extraction', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc directive docs extraction', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc docs extraction filtering', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc enum docs extraction', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc function docs extraction', () => {
Expand Down Expand Up @@ -113,5 +113,22 @@ runInEachFileSystem(os => {
expect(numberOverloadEntry.params[0].type).toBe('number');
expect(numberOverloadEntry.returnType).toBe('number');
});

it('should extract function generics', () => {
env.write('index.ts', `
export function save<T>(data: T) { }
`);

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

const [functionEntry] = docs as FunctionEntry[];
expect(functionEntry.generics.length).toBe(1);

const [genericEntry] = functionEntry.generics;
expect(genericEntry.name).toBe('T');
expect(genericEntry.constraint).toBeUndefined();
expect(genericEntry.default).toBeUndefined();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc interface docs extraction', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc jsdoc extraction', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc NgModule docs extraction', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc pipe docs extraction', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc re-export docs extraction', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {NgtscTestEnvironment} from '../env';

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

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

describe('ngtsc type alias docs extraction', () => {
Expand Down

0 comments on commit 634c529

Please sign in to comment.