mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
docs(docs-infra): read jsdoctags from function overloads (#58994)
Functions like `linkedSignal` have there `@developerPreview` tags on the overload signature. This commit adds the support for them. This commit also removes the logic for multiple entries, as now overloads are a single entry. fixes #58817 PR Close #58994
This commit is contained in:
parent
7c628f9ee8
commit
d0ea622040
5 changed files with 236 additions and 122 deletions
|
|
@ -1,5 +1,5 @@
|
|||
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
|
||||
import type {DocEntry, EntryCollection, JsDocTagEntry} from '@angular/compiler-cli';
|
||||
import type {DocEntry, EntryCollection, JsDocTagEntry, FunctionEntry} from '@angular/compiler-cli';
|
||||
|
||||
export interface ManifestEntry {
|
||||
name: string;
|
||||
|
|
@ -17,78 +17,37 @@ export type Manifest = {
|
|||
entries: ManifestEntry[];
|
||||
}[];
|
||||
|
||||
/** Gets a unique lookup key for an API, e.g. "@angular/core/ElementRef". */
|
||||
function getApiLookupKey(moduleName: string, name: string) {
|
||||
return `${moduleName}/${name}`;
|
||||
}
|
||||
/** Gets whether the given entry has a given JsDoc tag. */
|
||||
function hasTag(entry: DocEntry | FunctionEntry, tag: string, every = false) {
|
||||
const hasTagName = (t: JsDocTagEntry) => t.name === tag;
|
||||
|
||||
/** Gets whether the given entry has the "@deprecated" JsDoc tag. */
|
||||
function hasDeprecatedTag(entry: DocEntry) {
|
||||
return entry.jsdocTags.some((t: JsDocTagEntry) => t.name === 'deprecated');
|
||||
}
|
||||
if (every && 'signatures' in entry && entry.signatures.length > 1) {
|
||||
// For overloads we need to check all signatures.
|
||||
return entry.signatures.every((s) => s.jsdocTags.some(hasTagName));
|
||||
}
|
||||
|
||||
/** Gets whether the given entry has the "@developerPreview" JsDoc tag. */
|
||||
function hasDeveloperPreviewTag(entry: DocEntry) {
|
||||
return entry.jsdocTags.some((t: JsDocTagEntry) => t.name === 'developerPreview');
|
||||
}
|
||||
const jsdocTags = [
|
||||
...entry.jsdocTags,
|
||||
...((entry as FunctionEntry).signatures?.flatMap((s) => s.jsdocTags) ?? []),
|
||||
...((entry as FunctionEntry).implementation?.jsdocTags ?? []),
|
||||
];
|
||||
|
||||
/** Gets whether the given entry has the "@experimental" JsDoc tag. */
|
||||
function hasExperimentalTag(entry: DocEntry) {
|
||||
return entry.jsdocTags.some((t: JsDocTagEntry) => t.name === 'experimental');
|
||||
return jsdocTags.some(hasTagName);
|
||||
}
|
||||
|
||||
/** Gets whether the given entry is deprecated in the manifest. */
|
||||
function isDeprecated(
|
||||
lookup: Map<string, DocEntry[]>,
|
||||
moduleName: string,
|
||||
entry: DocEntry,
|
||||
): boolean {
|
||||
const entriesWithSameName = lookup.get(getApiLookupKey(moduleName, entry.name));
|
||||
|
||||
// If there are multiple entries with the same name in the same module, only
|
||||
// mark them as deprecated if *all* of the entries with the same name are
|
||||
// deprecated (e.g. function overloads).
|
||||
if (entriesWithSameName && entriesWithSameName.length > 1) {
|
||||
return entriesWithSameName.every((entry) => hasDeprecatedTag(entry));
|
||||
}
|
||||
|
||||
return hasDeprecatedTag(entry);
|
||||
function isDeprecated(entry: DocEntry): boolean {
|
||||
return hasTag(entry, 'deprecated', /* every */ true);
|
||||
}
|
||||
|
||||
/** Gets whether the given entry is hasDeveloperPreviewTag in the manifest. */
|
||||
function isDeveloperPreview(
|
||||
lookup: Map<string, DocEntry[]>,
|
||||
moduleName: string,
|
||||
entry: DocEntry,
|
||||
): boolean {
|
||||
const entriesWithSameName = lookup.get(getApiLookupKey(moduleName, entry.name));
|
||||
|
||||
// If there are multiple entries with the same name in the same module, only
|
||||
// mark them as developer preview if *all* of the entries with the same name
|
||||
// are hasDeveloperPreviewTag (e.g. function overloads).
|
||||
if (entriesWithSameName && entriesWithSameName.length > 1) {
|
||||
return entriesWithSameName.every((entry) => hasDeveloperPreviewTag(entry));
|
||||
}
|
||||
|
||||
return hasDeveloperPreviewTag(entry);
|
||||
function isDeveloperPreview(entry: DocEntry): boolean {
|
||||
return hasTag(entry, 'developerPreview');
|
||||
}
|
||||
|
||||
/** Gets whether the given entry is hasExperimentalTag in the manifest. */
|
||||
function isExperimental(
|
||||
lookup: Map<string, DocEntry[]>,
|
||||
moduleName: string,
|
||||
entry: DocEntry,
|
||||
): boolean {
|
||||
const entriesWithSameName = lookup.get(getApiLookupKey(moduleName, entry.name));
|
||||
|
||||
// If there are multiple entries with the same name in the same module, only
|
||||
// mark them as developer preview if *all* of the entries with the same name
|
||||
// are hasExperimentalTag (e.g. function overloads).
|
||||
if (entriesWithSameName && entriesWithSameName.length > 1) {
|
||||
return entriesWithSameName.every((entry) => hasExperimentalTag(entry));
|
||||
}
|
||||
|
||||
return hasExperimentalTag(entry);
|
||||
function isExperimental(entry: DocEntry): boolean {
|
||||
return hasTag(entry, 'experimental');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -96,31 +55,14 @@ function isExperimental(
|
|||
* extract_api_to_json.
|
||||
*/
|
||||
export function generateManifest(apiCollections: EntryCollection[]): Manifest {
|
||||
// Filter out repeated entries for function overloads, but also keep track of
|
||||
// all symbols keyed to their lookup key. We need this lookup later for
|
||||
// determining whether to mark an entry as deprecated.
|
||||
const entryLookup = new Map<string, DocEntry[]>();
|
||||
for (const collection of apiCollections) {
|
||||
collection.entries = collection.entries.filter((entry) => {
|
||||
const lookupKey = getApiLookupKey(collection.moduleName, entry.name);
|
||||
if (entryLookup.has(lookupKey)) {
|
||||
entryLookup.get(lookupKey)!.push(entry);
|
||||
return false;
|
||||
}
|
||||
|
||||
entryLookup.set(lookupKey, [entry]);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
const manifest: Manifest = [];
|
||||
for (const collection of apiCollections) {
|
||||
const entries = collection.entries.map((entry) => ({
|
||||
const entries = collection.entries.map((entry: DocEntry) => ({
|
||||
name: entry.name,
|
||||
type: entry.entryType,
|
||||
isDeprecated: isDeprecated(entryLookup, collection.moduleName, entry),
|
||||
isDeveloperPreview: isDeveloperPreview(entryLookup, collection.moduleName, entry),
|
||||
isExperimental: isExperimental(entryLookup, collection.moduleName, entry),
|
||||
isDeprecated: isDeprecated(entry),
|
||||
isDeveloperPreview: isDeveloperPreview(entry),
|
||||
isExperimental: isExperimental(entry),
|
||||
}));
|
||||
|
||||
const existingEntry = manifest.find((entry) => entry.moduleName === collection.moduleName);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
|
||||
import {DocEntry, EntryType, JsDocTagEntry} from '@angular/compiler-cli';
|
||||
import {DocEntry, EntryType, FunctionEntry, JsDocTagEntry} from '@angular/compiler-cli';
|
||||
import {generateManifest, Manifest} from '../generate_manifest';
|
||||
|
||||
describe('api manifest generation', () => {
|
||||
|
|
@ -178,44 +178,40 @@ describe('api manifest generation', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should deduplicate function overloads', () => {
|
||||
const manifest = generateManifest([
|
||||
{
|
||||
moduleName: '@angular/core',
|
||||
entries: [
|
||||
entry({name: 'save', entryType: EntryType.Function}),
|
||||
entry({name: 'save', entryType: EntryType.Function}),
|
||||
],
|
||||
normalizedModuleName: 'angular_core',
|
||||
moduleLabel: 'core',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(manifest).toEqual([
|
||||
{
|
||||
moduleName: '@angular/core',
|
||||
moduleLabel: 'core',
|
||||
normalizedModuleName: 'angular_core',
|
||||
entries: [
|
||||
{
|
||||
name: 'save',
|
||||
type: EntryType.Function,
|
||||
isDeprecated: false,
|
||||
isDeveloperPreview: false,
|
||||
isExperimental: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not mark a function as deprecated if only one overload is deprecated', () => {
|
||||
const manifest = generateManifest([
|
||||
{
|
||||
moduleName: '@angular/core',
|
||||
entries: [
|
||||
entry({name: 'save', entryType: EntryType.Function}),
|
||||
entry({name: 'save', entryType: EntryType.Function, jsdocTags: jsdocTags('deprecated')}),
|
||||
functionEntry({
|
||||
name: 'save',
|
||||
entryType: EntryType.Function,
|
||||
jsdocTags: [],
|
||||
signatures: [
|
||||
{
|
||||
name: 'save',
|
||||
returnType: 'void',
|
||||
jsdocTags: [],
|
||||
description: '',
|
||||
entryType: EntryType.Function,
|
||||
params: [],
|
||||
generics: [],
|
||||
isNewType: false,
|
||||
rawComment: '',
|
||||
},
|
||||
{
|
||||
name: 'save',
|
||||
returnType: 'void',
|
||||
jsdocTags: jsdocTags('deprecated'),
|
||||
description: '',
|
||||
entryType: EntryType.Function,
|
||||
params: [],
|
||||
generics: [],
|
||||
isNewType: false,
|
||||
rawComment: '',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
normalizedModuleName: 'angular_core',
|
||||
moduleLabel: 'core',
|
||||
|
|
@ -245,8 +241,35 @@ describe('api manifest generation', () => {
|
|||
{
|
||||
moduleName: '@angular/core',
|
||||
entries: [
|
||||
entry({name: 'save', entryType: EntryType.Function, jsdocTags: jsdocTags('deprecated')}),
|
||||
entry({name: 'save', entryType: EntryType.Function, jsdocTags: jsdocTags('deprecated')}),
|
||||
functionEntry({
|
||||
name: 'save',
|
||||
entryType: EntryType.Function,
|
||||
jsdocTags: [],
|
||||
signatures: [
|
||||
{
|
||||
name: 'save',
|
||||
returnType: 'void',
|
||||
jsdocTags: jsdocTags('deprecated'),
|
||||
description: '',
|
||||
entryType: EntryType.Function,
|
||||
params: [],
|
||||
generics: [],
|
||||
isNewType: false,
|
||||
rawComment: '',
|
||||
},
|
||||
{
|
||||
name: 'save',
|
||||
returnType: 'void',
|
||||
jsdocTags: jsdocTags('deprecated'),
|
||||
description: '',
|
||||
entryType: EntryType.Function,
|
||||
params: [],
|
||||
generics: [],
|
||||
isNewType: false,
|
||||
rawComment: '',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
normalizedModuleName: 'angular_core',
|
||||
moduleLabel: 'core',
|
||||
|
|
@ -376,6 +399,15 @@ function entry(patch: Partial<DocEntry>): DocEntry {
|
|||
};
|
||||
}
|
||||
|
||||
function functionEntry(patch: Partial<FunctionEntry>): FunctionEntry {
|
||||
return entry({
|
||||
entryType: EntryType.Function,
|
||||
implementation: [],
|
||||
signatures: [],
|
||||
...patch,
|
||||
} as FunctionEntry) as FunctionEntry;
|
||||
}
|
||||
|
||||
/** Creates a fake jsdoc tag entry list that contains a tag with the given name */
|
||||
function jsdocTags(name: string): JsDocTagEntry[] {
|
||||
return [{name, comment: ''}];
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
FunctionEntry,
|
||||
InitializerApiFunctionEntry,
|
||||
InterfaceEntry,
|
||||
JsDocTagEntry,
|
||||
MemberEntry,
|
||||
MemberType,
|
||||
MethodEntry,
|
||||
|
|
@ -125,7 +126,7 @@ export function isSetterEntry(entry: MemberEntry): entry is PropertyEntry {
|
|||
|
||||
/** Gets whether the given entry is deprecated. */
|
||||
export function isDeprecatedEntry<T extends HasJsDocTags>(entry: T) {
|
||||
return entry.jsdocTags.some((tag) => tag.name === 'deprecated');
|
||||
return hasTag(entry, 'deprecated', /* every */ true);
|
||||
}
|
||||
|
||||
export function getDeprecatedEntry<T extends HasJsDocTags>(entry: T) {
|
||||
|
|
@ -133,13 +134,30 @@ export function getDeprecatedEntry<T extends HasJsDocTags>(entry: T) {
|
|||
}
|
||||
|
||||
/** Gets whether the given entry is developer preview. */
|
||||
export function isDeveloperPreview<T extends HasJsDocTags>(entry: T) {
|
||||
return entry.jsdocTags.some((tag) => tag.name === 'developerPreview');
|
||||
export function isDeveloperPreview<T extends HasJsDocTags | FunctionEntry>(entry: T) {
|
||||
return hasTag(entry, 'developerPreview', false);
|
||||
}
|
||||
|
||||
/** Gets whether the given entry is is experimental. */
|
||||
export function isExperimental<T extends HasJsDocTags>(entry: T) {
|
||||
return entry.jsdocTags.some((tag) => tag.name === 'experimental');
|
||||
return hasTag(entry, 'experimental');
|
||||
}
|
||||
/** Gets whether the given entry has a given JsDoc tag. */
|
||||
function hasTag<T extends HasJsDocTags | FunctionEntry>(entry: T, tag: string, every = false) {
|
||||
const hasTagName = (t: JsDocTagEntry) => t.name === tag;
|
||||
|
||||
if (every && 'signatures' in entry && entry.signatures.length > 1) {
|
||||
// For overloads we need to check all signatures.
|
||||
return entry.signatures.every((s) => s.jsdocTags.some(hasTagName));
|
||||
}
|
||||
|
||||
const jsdocTags = [
|
||||
...entry.jsdocTags,
|
||||
...((entry as FunctionEntry).signatures?.flatMap((s) => s.jsdocTags) ?? []),
|
||||
...((entry as FunctionEntry).implementation?.jsdocTags ?? []),
|
||||
];
|
||||
|
||||
return jsdocTags.some(hasTagName);
|
||||
}
|
||||
|
||||
/** Gets whether the given entry is a cli entry. */
|
||||
|
|
|
|||
|
|
@ -669,6 +669,91 @@
|
|||
"startLine": 103,
|
||||
"endLine": 125
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "linkedSignal",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "linkedSignal",
|
||||
"entryType": "function",
|
||||
"description": "Creates a writable signals whose value is initialized and reset by the linked, reactive computation.",
|
||||
"generics": [{"name": "D"}],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [{"name": "developerPreview", "comment": ""}],
|
||||
"params": [
|
||||
{
|
||||
"name": "computation",
|
||||
"description": "",
|
||||
"type": "() => D",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"description": "",
|
||||
"type": "{ equal?: ValueEqualityFn<NoInfer<D>> | undefined; } | undefined",
|
||||
"isOptional": true,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * Creates a writable signals whose value is initialized and reset by the linked, reactive computation.\n *\n * @developerPreview\n */",
|
||||
"returnType": "WritableSignal<D>"
|
||||
},
|
||||
{
|
||||
"name": "linkedSignal",
|
||||
"entryType": "function",
|
||||
"description": "Creates a writable signals whose value is initialized and reset by the linked, reactive computation.\nThis is an advanced API form where the computation has access to the previous value of the signal and the computation result.",
|
||||
"generics": [{"name": "S"}, {"name": "D"}],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [{"name": "developerPreview", "comment": ""}],
|
||||
"params": [
|
||||
{
|
||||
"name": "options",
|
||||
"description": "",
|
||||
"type": "{ source: () => S; computation: (source: NoInfer<S>, previous?: { source: NoInfer<S>; value: NoInfer<D>; } | undefined) => D; equal?: ValueEqualityFn<NoInfer<D>> | undefined; }",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * Creates a writable signals whose value is initialized and reset by the linked, reactive computation.\n * This is an advanced API form where the computation has access to the previous value of the signal and the computation result.\n *\n * @developerPreview\n */",
|
||||
"returnType": "WritableSignal<D>"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [
|
||||
{
|
||||
"name": "optionsOrComputation",
|
||||
"description": "",
|
||||
"type": "{ source: () => S; computation: ComputationFn<S, D>; equal?: ValueEqualityFn<D> | undefined; } | (() => D)",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"description": "",
|
||||
"type": "{ equal?: ValueEqualityFn<D> | undefined; } | undefined",
|
||||
"isOptional": true,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"isNewType": false,
|
||||
"returnType": "WritableSignal<D>",
|
||||
"generics": [{"name": "S"}, {"name": "D"}],
|
||||
"name": "linkedSignal",
|
||||
"description": "",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": ""
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "",
|
||||
"source": {
|
||||
"filePath": "/packages/core/src/render3/reactivity/linked_signal.ts",
|
||||
"startLine": 112,
|
||||
"endLine": 115
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import {runfiles} from '@bazel/runfiles';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {getRenderable} from '../processing';
|
||||
import {DocEntryRenderable} from '../entities/renderables';
|
||||
import {initHighlighter} from '../shiki/shiki';
|
||||
import {configureMarkedGlobally} from '../marked/configuration';
|
||||
|
||||
// Note: The tests will probably break if the schema of the api extraction changes.
|
||||
// All entries in the fake-entries are extracted from Angular's api.
|
||||
// You can just generate them an copy/replace the items in the fake-entries file.
|
||||
|
||||
describe('renderable', () => {
|
||||
const entries = new Map<string, DocEntryRenderable>();
|
||||
|
||||
beforeAll(async () => {
|
||||
await initHighlighter();
|
||||
await configureMarkedGlobally();
|
||||
|
||||
const entryContent = await readFile(runfiles.resolvePackageRelative('fake-entries.json'), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const entryJson = JSON.parse(entryContent) as any;
|
||||
for (const entry of entryJson.entries) {
|
||||
const renderableJson = getRenderable(entry, '@angular/fakeentry') as DocEntryRenderable;
|
||||
entries.set(entry['name'], renderableJson);
|
||||
}
|
||||
});
|
||||
|
||||
it('should compute the flags correctly', () => {
|
||||
// linkedSignal has the developerPreview tag on the overloads not on the main entry.
|
||||
const linkedSignal = entries.get('linkedSignal');
|
||||
expect(linkedSignal).toBeDefined();
|
||||
expect(linkedSignal!.isDeprecated).toBe(false);
|
||||
expect(linkedSignal!.isDeveloperPreview).toBe(true);
|
||||
expect(linkedSignal!.isExperimental).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue