Skip to content

Commit f00336c

Browse files
crisbetoalxhub
authored andcommitted
refactor(compiler-cli): add the ability to treat object literals as enums in docs (#54487)
We have a couple of cases now (#53753 and #54414) where we're forced to redefine enums as object literals. These literals aren't rendered in the best way in the docs so these changes introduce a new `object-literal-as-enum` tag that we can use to mark them so they're treated for documentation purposes. PR Close #54487
1 parent 8997260 commit f00336c

2 files changed

Lines changed: 100 additions & 8 deletions

File tree

packages/compiler-cli/src/ngtsc/docs/src/constant_extractor.ts

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88

99
import ts from 'typescript';
1010

11-
import {ConstantEntry, EntryType} from './entities';
12-
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc,} from './jsdoc_extractor';
11+
import {ConstantEntry, EntryType, EnumEntry, EnumMemberEntry, MemberType} from './entities';
12+
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor';
13+
14+
/** Name of the tag indicating that an object literal should be shown as an enum in docs. */
15+
const LITERAL_AS_ENUM_TAG = 'object-literal-as-enum';
1316

1417
/** Extracts documentation entry for a constant. */
1518
export function extractConstant(
16-
declaration: ts.VariableDeclaration, typeChecker: ts.TypeChecker): ConstantEntry {
19+
declaration: ts.VariableDeclaration, typeChecker: ts.TypeChecker): ConstantEntry|EnumEntry {
1720
// For constants specifically, we want to get the base type for any literal types.
1821
// For example, TypeScript by default extracts `const PI = 3.14` as PI having a type of the
1922
// literal `3.14`. We don't want this behavior for constants, since generally one wants the
@@ -26,20 +29,71 @@ export function extractConstant(
2629
// In the TS AST, the leading comment for a variable declaration is actually
2730
// on the ancestor `ts.VariableStatement` (since a single variable statement may
2831
// contain multiple variable declarations).
29-
const variableStatement = declaration.parent.parent;
3032
const rawComment = extractRawJsDoc(declaration.parent.parent);
33+
const jsdocTags = extractJsDocTags(declaration);
34+
const description = extractJsDocDescription(declaration);
35+
const name = declaration.name.getText();
36+
37+
// Some constants have to be treated as enums for documentation purposes.
38+
if (jsdocTags.some(tag => tag.name === LITERAL_AS_ENUM_TAG)) {
39+
return {
40+
name,
41+
entryType: EntryType.Enum,
42+
members: extractLiteralPropertiesAsEnumMembers(declaration),
43+
rawComment,
44+
description,
45+
jsdocTags: jsdocTags.filter(tag => tag.name !== LITERAL_AS_ENUM_TAG),
46+
};
47+
}
3148

3249
return {
33-
name: declaration.name.getText(),
50+
name: name,
3451
type: typeChecker.typeToString(resolvedType),
3552
entryType: EntryType.Constant,
3653
rawComment,
37-
description: extractJsDocDescription(declaration),
38-
jsdocTags: extractJsDocTags(declaration),
54+
description,
55+
jsdocTags,
3956
};
4057
}
4158

4259
/** Gets whether a given constant is an Angular-added const that should be ignored for docs. */
4360
export function isSyntheticAngularConstant(declaration: ts.VariableDeclaration) {
4461
return declaration.name.getText() === 'USED_FOR_NG_TYPE_CHECKING';
4562
}
63+
64+
65+
/**
66+
* Extracts the properties of a variable initialized as an object literal as if they were enum
67+
* members. Will throw for any variables that can't be statically analyzed easily.
68+
*/
69+
function extractLiteralPropertiesAsEnumMembers(declaration: ts.VariableDeclaration):
70+
EnumMemberEntry[] {
71+
const initializer = declaration.initializer;
72+
73+
if (initializer === undefined || !ts.isObjectLiteralExpression(initializer)) {
74+
throw new Error(`Declaration tagged with "${
75+
LITERAL_AS_ENUM_TAG}" must be initialized to an object literal`);
76+
}
77+
78+
return initializer.properties.map(prop => {
79+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
80+
throw new Error(`Property in declaration tagged with "${
81+
LITERAL_AS_ENUM_TAG}" must be a property assignment with a static name`);
82+
}
83+
84+
if (!ts.isNumericLiteral(prop.initializer) && !ts.isStringLiteralLike(prop.initializer)) {
85+
throw new Error(`Property in declaration tagged with "${
86+
LITERAL_AS_ENUM_TAG}" must be initialized to a number or string literal`);
87+
}
88+
89+
return ({
90+
name: prop.name.text,
91+
type: `${declaration.name.getText()}.${prop.name.text}`,
92+
value: prop.initializer.getText(),
93+
memberType: MemberType.EnumItem,
94+
jsdocTags: extractJsDocTags(prop),
95+
description: extractJsDocDescription(prop),
96+
memberTags: [],
97+
});
98+
});
99+
}

packages/compiler-cli/test/ngtsc/doc_extraction/constant_doc_extraction_spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs';
10-
import {ConstantEntry, EntryType} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
10+
import {ConstantEntry, EntryType, EnumEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
1111
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
1212
import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';
1313

@@ -77,5 +77,43 @@ runInEachFileSystem(() => {
7777
expect(typedToken.entryType).toBe(EntryType.Constant);
7878
expect(typedToken.type).toBe('InjectionToken<string>');
7979
});
80+
81+
it('should extract an object literal marked as an enum', () => {
82+
env.write('index.ts', `
83+
/**
84+
* Toppings for your pizza.
85+
* @object-literal-as-enum
86+
*/
87+
export const PizzaTopping = {
88+
/** It is cheese */
89+
Cheese: 0,
90+
91+
/** Or "tomato" if you are British */
92+
Tomato: "tomato",
93+
}
94+
`);
95+
96+
const docs: DocEntry[] = env.driveDocsExtraction('index.ts');
97+
98+
expect(docs.length).toBe(1);
99+
expect(docs[0].entryType).toBe(EntryType.Enum);
100+
expect(docs[0].jsdocTags).toEqual([]);
101+
102+
const enumEntry = docs[0] as EnumEntry;
103+
expect(enumEntry.name).toBe('PizzaTopping');
104+
expect(enumEntry.members.length).toBe(2);
105+
106+
const [cheeseEntry, tomatoEntry] = enumEntry.members;
107+
108+
expect(cheeseEntry.name).toBe('Cheese');
109+
expect(cheeseEntry.description).toBe('It is cheese');
110+
expect(cheeseEntry.value).toBe('0');
111+
expect(cheeseEntry.type).toBe('PizzaTopping.Cheese');
112+
113+
expect(tomatoEntry.name).toBe('Tomato');
114+
expect(tomatoEntry.description).toBe('Or "tomato" if you are British');
115+
expect(tomatoEntry.value).toBe('"tomato"');
116+
expect(tomatoEntry.type).toBe('PizzaTopping.Tomato');
117+
});
80118
});
81119
});

0 commit comments

Comments
 (0)