Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: show overloads for functions #55565

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion adev/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ APPLICATION_ASSETS = [
]

APPLICATION_DEPS = [
"@npm//@angular/build",
"@npm//@angular-devkit/build-angular",
"@npm//@angular/animations",
"@npm//@angular/build",
"@npm//@angular/cdk",
"@npm//@angular/common",
"@npm//@angular/compiler",
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler-cli/src/ngtsc/docs/src/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ export interface FunctionEntry extends DocEntry {
isNewType: boolean;
}

export interface FunctionWithOverloadsEntry extends FunctionEntry {
Copy link
Member

Choose a reason for hiding this comment

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

We should instead re-use what I've built for initializer APIs: FunctionWithOverloads

Copy link
Member

Choose a reason for hiding this comment

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

I think function extractor should instead return such entry, which has clear implementation + signatures

Copy link
Member Author

@JeanMeche JeanMeche May 4, 2024

Choose a reason for hiding this comment

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

I thought about reusing it too but went with another type because the extractor needs to return a DocEntry which FunctionWithOverloads doesnt extend.

I'm still open to suggestions.

Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking about an entry like we have for initializer APIs, with clear separation of implementation + signatures. Maybe we can share that one and use for both? and name it FunctionEntry, while the existing FunctionEntry becomes FunctionMetadata or similar

overloads: FunctionEntry[] | null;
}

/** Sub-entry for a single class or enum member. */
export interface MemberEntry {
name: string;
Expand Down
18 changes: 0 additions & 18 deletions packages/compiler-cli/src/ngtsc/docs/src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,6 @@ export class DocsExtractor {
([exportName, declaration]) => [exportName, declaration.node] as const,
);

// Cache the declaration count since we're going to be appending more declarations as
// we iterate.
const declarationCount = exportedDeclarations.length;

// The exported declaration map only includes one function declaration in situations
// where a function has overloads, so we add the overloads here.
for (let i = 0; i < declarationCount; i++) {
const [exportName, declaration] = exportedDeclarations[i];
if (ts.isFunctionDeclaration(declaration)) {
const extractor = new FunctionExtractor(exportName, declaration, this.typeChecker);
const overloads = extractor
.getOverloads()
JeanMeche marked this conversation as resolved.
Show resolved Hide resolved
.map((overload) => [exportName, overload] as const);

exportedDeclarations.push(...overloads);
}
}

// 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.
return exportedDeclarations.sort(
Expand Down
113 changes: 67 additions & 46 deletions packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import ts from 'typescript';

import {EntryType, FunctionEntry, ParameterEntry} from './entities';
import {EntryType, FunctionWithOverloadsEntry, ParameterEntry} from './entities';
import {extractGenerics} from './generics_extractor';
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor';
import {extractResolvedTypeString} from './type_extractor';
Expand All @@ -23,66 +23,38 @@ export type FunctionLike =
export class FunctionExtractor {
constructor(
private name: string,
private declaration: FunctionLike,
private exportDeclaration: FunctionLike,
private typeChecker: ts.TypeChecker,
) {}

extract(): FunctionEntry {
extract(): FunctionWithOverloadsEntry {
// TODO: is there any real situation in which the signature would not be available here?
// Is void a better type?
const signature = this.typeChecker.getSignatureFromDeclaration(this.declaration);
const signature = this.typeChecker.getSignatureFromDeclaration(this.exportDeclaration);
const returnType = signature
? this.typeChecker.typeToString(this.typeChecker.getReturnTypeOfSignature(signature))
: 'unknown';

const implementation =
findImplementationOfFunction(this.exportDeclaration, this.typeChecker) ??
this.exportDeclaration;

const type = this.typeChecker.getTypeAtLocation(this.exportDeclaration);
const overloads = extractOverloadSignatures(this.name, this.typeChecker, type);

return {
params: extractAllParams(this.declaration.parameters, this.typeChecker),
params: extractAllParams(implementation.parameters, this.typeChecker),
name: this.name,
isNewType: ts.isConstructSignatureDeclaration(this.declaration),
isNewType: ts.isConstructSignatureDeclaration(implementation),
returnType,
entryType: EntryType.Function,
generics: extractGenerics(this.declaration),
description: extractJsDocDescription(this.declaration),
jsdocTags: extractJsDocTags(this.declaration),
rawComment: extractRawJsDoc(this.declaration),
generics: extractGenerics(implementation),
description: extractJsDocDescription(implementation),
jsdocTags: extractJsDocTags(implementation),
rawComment: extractRawJsDoc(implementation),
overloads: overloads.length > 1 ? overloads : null,
};
}

/** Gets all overloads for the function (excluding this extractor's FunctionDeclaration). */
getOverloads(): ts.FunctionDeclaration[] {
const overloads = [];

// The symbol for this declaration has reference to the other function declarations for
// the overloads.
const symbol = this.getSymbol();

const declarationCount = symbol?.declarations?.length ?? 0;
if (declarationCount > 1) {
// Stop iterating before the final declaration, which is the actual implementation.
for (let i = 0; i < declarationCount - 1; i++) {
const overloadDeclaration = symbol?.declarations?.[i];

// Skip the declaration we started with.
if (overloadDeclaration?.pos === this.declaration.pos) continue;

if (
overloadDeclaration &&
ts.isFunctionDeclaration(overloadDeclaration) &&
overloadDeclaration.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)
) {
overloads.push(overloadDeclaration);
}
}
}

return overloads;
}

private getSymbol(): ts.Symbol | undefined {
return this.typeChecker
.getSymbolsInScope(this.declaration, ts.SymbolFlags.Function)
.find((s) => s.name === this.declaration.name?.getText());
}
}

/** Extracts parameters of the given parameter declaration AST nodes. */
Expand All @@ -98,3 +70,52 @@ export function extractAllParams(
isRestParam: !!param.dotDotDotToken,
}));
}

/** Filters the list signatures to valid initializer API signatures. */
export function filterSignatureDeclarations(signatures: readonly ts.Signature[]) {
JeanMeche marked this conversation as resolved.
Show resolved Hide resolved
const result: Array<ts.FunctionDeclaration | ts.CallSignatureDeclaration> = [];
for (const signature of signatures) {
const decl = signature.getDeclaration();
if (ts.isFunctionDeclaration(decl) || ts.isCallSignatureDeclaration(decl)) {
result.push(decl);
}
}
return result;
}

export function extractOverloadSignatures(
name: string,
typeChecker: ts.TypeChecker,
type: ts.Type,
) {
return filterSignatureDeclarations(type.getCallSignatures()).map((s) => ({
name: name,
entryType: EntryType.Function,
description: extractJsDocDescription(s),
generics: extractGenerics(s),
isNewType: false,
jsdocTags: extractJsDocTags(s),
params: extractAllParams(s.parameters, typeChecker),
rawComment: extractRawJsDoc(s),
returnType: typeChecker.typeToString(
typeChecker.getReturnTypeOfSignature(typeChecker.getSignatureFromDeclaration(s)!),
),
}));
}

/** Finds the implementation of the given function declaration overload signature. */
export function findImplementationOfFunction(
node: FunctionLike,
typeChecker: ts.TypeChecker,
): FunctionLike | undefined {
if ((node as ts.FunctionDeclaration).body !== undefined || node.name === undefined) {
return node;
}

const symbol = typeChecker.getSymbolAtLocation(node.name);
const implementation = symbol?.declarations?.find(
(s): s is ts.FunctionDeclaration => ts.isFunctionDeclaration(s) && s.body !== undefined,
);

return implementation;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import {
InitializerApiFunctionEntry,
JsDocTagEntry,
} from './entities';
import {extractAllParams} from './function_extractor';
import {
extractAllParams,
extractOverloadSignatures,
findImplementationOfFunction,
} from './function_extractor';
import {extractGenerics} from './generics_extractor';
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor';

Expand Down Expand Up @@ -76,11 +80,7 @@ export function extractInitializerApiFunction(
const type = typeChecker.getTypeAtLocation(node);

// Top-level call signatures. E.g. `input()`, `input<ReadT>(initialValue: ReadT)`. etc.
const callFunction: FunctionWithOverloads = extractFunctionWithOverloads(
name,
type.getCallSignatures(),
typeChecker,
);
const callFunction: FunctionWithOverloads = extractFunctionWithOverloads(name, type, typeChecker);
// Sub-functions like `input.required()`.
const subFunctions: FunctionWithOverloads[] = [];

Expand All @@ -94,9 +94,7 @@ export function extractInitializerApiFunction(
}

const subType = typeChecker.getTypeAtLocation(subDecl);
subFunctions.push(
extractFunctionWithOverloads(subName, subType.getCallSignatures(), typeChecker),
);
subFunctions.push(extractFunctionWithOverloads(subName, subType, typeChecker));
}

let jsdocTags: JsDocTagEntry[];
Expand Down Expand Up @@ -185,18 +183,6 @@ function getContainerVariableStatement(node: ts.VariableDeclaration): ts.Variabl
return node.parent.parent;
}

/** Filters the list signatures to valid initializer API signatures. */
function filterSignatureDeclarations(signatures: readonly ts.Signature[]) {
const result: Array<ts.FunctionDeclaration | ts.CallSignatureDeclaration> = [];
for (const signature of signatures) {
const decl = signature.getDeclaration();
if (ts.isFunctionDeclaration(decl) || ts.isCallSignatureDeclaration(decl)) {
result.push(decl);
}
}
return result;
}

/**
* Extracts all given signatures and returns them as a function with
* overloads.
Expand All @@ -208,40 +194,13 @@ function filterSignatureDeclarations(signatures: readonly ts.Signature[]) {
*/
function extractFunctionWithOverloads(
name: string,
signatures: readonly ts.Signature[],
type: ts.Type,
typeChecker: ts.TypeChecker,
): FunctionWithOverloads {
return {
name,
signatures: filterSignatureDeclarations(signatures).map((s) => ({
name,
entryType: EntryType.Function,
description: extractJsDocDescription(s),
generics: extractGenerics(s),
isNewType: false,
jsdocTags: extractJsDocTags(s),
params: extractAllParams(s.parameters, typeChecker),
rawComment: extractRawJsDoc(s),
returnType: typeChecker.typeToString(
typeChecker.getReturnTypeOfSignature(typeChecker.getSignatureFromDeclaration(s)!),
),
})),
signatures: extractOverloadSignatures(name, typeChecker, type),
// Implementation may be populated later.
implementation: null,
};
}

/** Finds the implementation of the given function declaration overload signature. */
function findImplementationOfFunction(
node: ts.FunctionDeclaration,
typeChecker: ts.TypeChecker,
): ts.FunctionDeclaration | undefined {
if (node.body !== undefined || node.name === undefined) {
return node;
}

const symbol = typeChecker.getSymbolAtLocation(node.name);
return symbol?.declarations?.find(
(s): s is ts.FunctionDeclaration => ts.isFunctionDeclaration(s) && s.body !== undefined,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
*/

import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs';
import {EntryType, FunctionEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
import {
EntryType,
FunctionEntry,
FunctionWithOverloadsEntry,
} 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 @@ -110,10 +114,10 @@ runInEachFileSystem(() => {
`,
);

const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
expect(docs.length).toBe(2);
const docs = env.driveDocsExtraction('index.ts') as (DocEntry & FunctionWithOverloadsEntry)[];
expect(docs[0].overloads?.length).toBe(2);

const [booleanOverloadEntry, numberOverloadEntry] = docs as FunctionEntry[];
const [booleanOverloadEntry, numberOverloadEntry] = docs[0].overloads!;

expect(booleanOverloadEntry.name).toBe('ident');
expect(booleanOverloadEntry.params.length).toBe(1);
Expand Down