Skip to content

Commit b1d2cef

Browse files
[js-api-parser] add interface generator (#13453)
1 parent 447ef0d commit b1d2cef

File tree

5 files changed

+803
-98
lines changed

5 files changed

+803
-98
lines changed

tools/apiview/parsers/js-api-parser/src/tokenGenerators/function.ts

Lines changed: 14 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,17 @@ import {
22
ApiFunction,
33
ApiItem,
44
ApiItemKind,
5-
ExcerptToken,
6-
ExcerptTokenKind,
75
Parameter,
86
TypeParameter,
97
} from "@microsoft/api-extractor-model";
108
import { ReviewToken, TokenKind } from "../models";
119
import { TokenGenerator } from "./index";
10+
import { createToken, processExcerptTokens } from "./helpers";
1211

1312
function isValid(item: ApiItem): item is ApiFunction {
1413
return item.kind === ApiItemKind.Function;
1514
}
1615

17-
/** Helper to create a token with common properties */
18-
function createToken(
19-
kind: TokenKind,
20-
value: string,
21-
options?: {
22-
hasSuffixSpace?: boolean;
23-
hasPrefixSpace?: boolean;
24-
navigateToId?: string;
25-
deprecated?: boolean;
26-
},
27-
): ReviewToken {
28-
return {
29-
Kind: kind,
30-
Value: value,
31-
HasSuffixSpace: options?.hasSuffixSpace ?? false,
32-
HasPrefixSpace: options?.hasPrefixSpace ?? false,
33-
NavigateToId: options?.navigateToId,
34-
IsDeprecated: options?.deprecated,
35-
};
36-
}
37-
38-
/**
39-
* Determines if a token needs a leading space based on its value
40-
* @param value The token value
41-
* @returns true if the token needs a leading space
42-
*/
43-
function needsLeadingSpace(value: string): boolean {
44-
return value === "|" || value === "&" || value === "is";
45-
}
46-
47-
/**
48-
* Determines if a token needs a trailing space based on its value
49-
* @param value The token value
50-
* @returns true if the token needs a trailing space
51-
*/
52-
function needsTrailingSpace(value: string): boolean {
53-
return value === "|" || value === "&" || value === "is";
54-
}
55-
56-
/** Process excerpt tokens and add them to the tokens array */
57-
function processExcerptTokens(
58-
excerptTokens: readonly ExcerptToken[],
59-
tokens: ReviewToken[],
60-
deprecated?: boolean,
61-
): void {
62-
for (const excerpt of excerptTokens) {
63-
const trimmedText = excerpt.text.trim();
64-
if (!trimmedText) continue;
65-
66-
const hasPrefixSpace = needsLeadingSpace(trimmedText);
67-
const hasSuffixSpace = needsTrailingSpace(trimmedText);
68-
69-
if (excerpt.kind === ExcerptTokenKind.Reference && excerpt.canonicalReference) {
70-
tokens.push(
71-
createToken(TokenKind.TypeName, trimmedText, {
72-
navigateToId: excerpt.canonicalReference.toString(),
73-
hasPrefixSpace,
74-
hasSuffixSpace,
75-
deprecated,
76-
}),
77-
);
78-
} else {
79-
tokens.push(
80-
createToken(TokenKind.Text, trimmedText, {
81-
hasPrefixSpace,
82-
hasSuffixSpace,
83-
deprecated,
84-
}),
85-
);
86-
}
87-
}
88-
}
89-
9016
function generate(item: ApiFunction, deprecated?: boolean): ReviewToken[] {
9117
const tokens: ReviewToken[] = [];
9218
if (item.kind !== ApiItemKind.Function) {
@@ -96,11 +22,8 @@ function generate(item: ApiFunction, deprecated?: boolean): ReviewToken[] {
9622
}
9723

9824
// Extract structured properties
99-
const parameters = (item as unknown as { readonly parameters: ReadonlyArray<Parameter> })
100-
.parameters;
101-
const typeParameters = (
102-
item as unknown as { readonly typeParameters: ReadonlyArray<TypeParameter> }
103-
).typeParameters;
25+
const parameters = item.parameters;
26+
const typeParameters = item.typeParameters;
10427

10528
// Add export and function keywords
10629
tokens.push(createToken(TokenKind.Keyword, "export", { hasSuffixSpace: true, deprecated }));
@@ -131,6 +54,17 @@ function generate(item: ApiFunction, deprecated?: boolean): ReviewToken[] {
13154
tokens.push(createToken(TokenKind.Text, tp.constraintExcerpt.text.trim(), { deprecated }));
13255
}
13356

57+
if (tp.defaultTypeExcerpt?.text.trim()) {
58+
tokens.push(
59+
createToken(TokenKind.Text, "=", {
60+
hasPrefixSpace: true,
61+
hasSuffixSpace: true,
62+
deprecated,
63+
}),
64+
);
65+
processExcerptTokens(tp.defaultTypeExcerpt.spannedTokens, tokens, deprecated);
66+
}
67+
13468
if (index < typeParameters.length - 1) {
13569
tokens.push(createToken(TokenKind.Text, ",", { hasSuffixSpace: true, deprecated }));
13670
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ExcerptToken, ExcerptTokenKind } from "@microsoft/api-extractor-model";
2+
import { ReviewToken, TokenKind } from "../models";
3+
4+
/** Helper to create a token with common properties */
5+
export function createToken(
6+
kind: TokenKind,
7+
value: string,
8+
options?: {
9+
hasSuffixSpace?: boolean;
10+
hasPrefixSpace?: boolean;
11+
navigateToId?: string;
12+
deprecated?: boolean;
13+
},
14+
): ReviewToken {
15+
return {
16+
Kind: kind,
17+
Value: value,
18+
HasSuffixSpace: options?.hasSuffixSpace ?? false,
19+
HasPrefixSpace: options?.hasPrefixSpace ?? false,
20+
NavigateToId: options?.navigateToId,
21+
IsDeprecated: options?.deprecated,
22+
};
23+
}
24+
25+
/**
26+
* Determines if a token needs a leading space based on its value
27+
* @param value The token value
28+
* @returns true if the token needs a leading space
29+
*/
30+
export function needsLeadingSpace(value: string): boolean {
31+
return value === "|" || value === "&" || value === "is" || value === "extends";
32+
}
33+
34+
/**
35+
* Determines if a token needs a trailing space based on its value
36+
* @param value The token value
37+
* @returns true if the token needs a trailing space
38+
*/
39+
export function needsTrailingSpace(value: string): boolean {
40+
return value === "|" || value === "&" || value === "is" || value === "extends";
41+
}
42+
43+
/** Process excerpt tokens and add them to the tokens array */
44+
export function processExcerptTokens(
45+
excerptTokens: readonly ExcerptToken[],
46+
tokens: ReviewToken[],
47+
deprecated?: boolean,
48+
): void {
49+
for (const excerpt of excerptTokens) {
50+
const trimmedText = excerpt.text.trim();
51+
if (!trimmedText) continue;
52+
53+
const hasPrefixSpace = needsLeadingSpace(trimmedText);
54+
const hasSuffixSpace = needsTrailingSpace(trimmedText);
55+
56+
if (excerpt.kind === ExcerptTokenKind.Reference && excerpt.canonicalReference) {
57+
tokens.push(
58+
createToken(TokenKind.TypeName, trimmedText, {
59+
navigateToId: excerpt.canonicalReference.toString(),
60+
hasPrefixSpace,
61+
hasSuffixSpace,
62+
deprecated,
63+
}),
64+
);
65+
} else {
66+
tokens.push(
67+
createToken(TokenKind.Text, trimmedText, {
68+
hasPrefixSpace,
69+
hasSuffixSpace,
70+
deprecated,
71+
}),
72+
);
73+
}
74+
}
75+
}
Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
11
import { enumTokenGenerator } from "./enum";
2-
import { ReviewToken } from '../models';
3-
import { ApiItem } from '@microsoft/api-extractor-model';
2+
import { ReviewToken } from "../models";
3+
import { ApiItem } from "@microsoft/api-extractor-model";
44
import { functionTokenGenerator } from "./function";
5+
import { interfaceTokenGenerator } from "./interfaces";
56

67
/**
78
* Interface for token generators that create ReviewTokens from ApiItems.
89
*/
910
export interface TokenGenerator<T extends ApiItem = ApiItem> {
10-
/**
11-
* Validates if the given ApiItem can be processed by this token generator.
12-
* @param item - The ApiItem to validate.
13-
* @returns True if the item is valid; otherwise, false.
14-
*/
15-
isValid(item: ApiItem): item is T;
11+
/**
12+
* Validates if the given ApiItem can be processed by this token generator.
13+
* @param item - The ApiItem to validate.
14+
* @returns True if the item is valid; otherwise, false.
15+
*/
16+
isValid(item: ApiItem): item is T;
1617

17-
/**
18-
* Generates ReviewTokens from the given ApiItem.
19-
* @param item - The ApiItem to process.
20-
* @param deprecated - Indicates if the Api is deprecated.
21-
* @returns An array of ReviewTokens generated from the ApiItem.
22-
*/
23-
generate(item: T, deprecated?: boolean): ReviewToken[];
18+
/**
19+
* Generates ReviewTokens from the given ApiItem.
20+
* @param item - The ApiItem to process.
21+
* @param deprecated - Indicates if the Api is deprecated.
22+
* @returns An array of ReviewTokens generated from the ApiItem.
23+
*/
24+
generate(item: T, deprecated?: boolean): ReviewToken[];
2425
}
2526

2627
export const generators: TokenGenerator[] = [
27-
enumTokenGenerator,
28-
functionTokenGenerator
29-
];
28+
enumTokenGenerator,
29+
functionTokenGenerator,
30+
interfaceTokenGenerator,
31+
];
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { ApiInterface, ApiItem, ApiItemKind } from "@microsoft/api-extractor-model";
2+
import { ReviewToken, TokenKind } from "../models";
3+
import { TokenGenerator } from "./index";
4+
import { createToken, processExcerptTokens } from "./helpers";
5+
6+
function isValid(item: ApiItem): item is ApiInterface {
7+
return item.kind === ApiItemKind.Interface;
8+
}
9+
10+
function generate(item: ApiInterface, deprecated?: boolean): ReviewToken[] {
11+
const tokens: ReviewToken[] = [];
12+
if (item.kind !== ApiItemKind.Interface) {
13+
throw new Error(
14+
`Invalid item ${item.displayName} of kind ${item.kind} for Interface token generator.`,
15+
);
16+
}
17+
18+
// Extract structured properties
19+
const typeParameters = item.typeParameters;
20+
21+
// Add export and interface keywords
22+
tokens.push(createToken(TokenKind.Keyword, "export", { hasSuffixSpace: true, deprecated }));
23+
24+
// Check for default export
25+
const isDefaultExport = item.excerptTokens.some((t) => t.text.includes("export default"));
26+
if (isDefaultExport) {
27+
tokens.push(createToken(TokenKind.Keyword, "default", { hasSuffixSpace: true, deprecated }));
28+
}
29+
30+
tokens.push(createToken(TokenKind.Keyword, "interface", { hasSuffixSpace: true, deprecated }));
31+
32+
// Create interface name token with proper metadata (matching splitAndBuild behavior)
33+
const nameToken = createToken(TokenKind.TypeName, item.displayName, { deprecated });
34+
nameToken.NavigateToId = item.canonicalReference.toString();
35+
nameToken.NavigationDisplayName = item.displayName;
36+
nameToken.RenderClasses = ["interface"];
37+
tokens.push(nameToken);
38+
39+
// Add type parameters
40+
if (typeParameters?.length > 0) {
41+
tokens.push(createToken(TokenKind.Text, "<", { deprecated }));
42+
typeParameters.forEach((tp, index) => {
43+
tokens.push(createToken(TokenKind.TypeName, tp.name, { deprecated }));
44+
45+
if (tp.constraintExcerpt?.text.trim()) {
46+
tokens.push(
47+
createToken(TokenKind.Keyword, "extends", {
48+
hasPrefixSpace: true,
49+
hasSuffixSpace: true,
50+
deprecated,
51+
}),
52+
);
53+
processExcerptTokens(tp.constraintExcerpt.spannedTokens, tokens, deprecated);
54+
}
55+
56+
if (tp.defaultTypeExcerpt?.text.trim()) {
57+
tokens.push(
58+
createToken(TokenKind.Text, "=", {
59+
hasPrefixSpace: true,
60+
hasSuffixSpace: true,
61+
deprecated,
62+
}),
63+
);
64+
processExcerptTokens(tp.defaultTypeExcerpt.spannedTokens, tokens, deprecated);
65+
}
66+
67+
if (index < typeParameters.length - 1) {
68+
tokens.push(createToken(TokenKind.Text, ",", { hasSuffixSpace: true, deprecated }));
69+
}
70+
});
71+
tokens.push(createToken(TokenKind.Text, ">", { deprecated }));
72+
}
73+
74+
// Add extends clause if interface extends other interfaces
75+
if (item.extendsTypes && item.extendsTypes.length > 0) {
76+
tokens.push(
77+
createToken(TokenKind.Keyword, "extends", {
78+
hasPrefixSpace: true,
79+
hasSuffixSpace: true,
80+
deprecated,
81+
}),
82+
);
83+
84+
item.extendsTypes.forEach((extendsType, index) => {
85+
processExcerptTokens(extendsType.excerpt.spannedTokens, tokens, deprecated);
86+
87+
if (index < item.extendsTypes.length - 1) {
88+
tokens.push(createToken(TokenKind.Text, ",", { hasSuffixSpace: true, deprecated }));
89+
}
90+
});
91+
}
92+
93+
return tokens;
94+
}
95+
96+
export const interfaceTokenGenerator: TokenGenerator<ApiInterface> = {
97+
isValid,
98+
generate,
99+
};

0 commit comments

Comments
 (0)