Skip to content

Commit

Permalink
docs(docs-infra): read jsdoctags from function overloads (angular#58994)
Browse files Browse the repository at this point in the history
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 angular#58817

PR Close angular#58994
  • Loading branch information
JeanMeche authored and pkozlowski-opensource committed Dec 2, 2024
1 parent 7c628f9 commit d0ea622
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 122 deletions.
106 changes: 24 additions & 82 deletions adev/shared-docs/pipeline/api-gen/manifest/generate_manifest.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,110 +17,52 @@ 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');
}

/**
* Generates an API manifest for a set of API collections extracted by
* 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);
Expand Down
104 changes: 68 additions & 36 deletions adev/shared-docs/pipeline/api-gen/manifest/test/manifest.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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: ''}];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
FunctionEntry,
InitializerApiFunctionEntry,
InterfaceEntry,
JsDocTagEntry,
MemberEntry,
MemberType,
MethodEntry,
Expand Down Expand Up @@ -125,21 +126,38 @@ 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) {
return entry.jsdocTags.find((tag) => tag.name === 'deprecated')?.comment ?? null;
}

/** 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. */
Expand Down
Loading

0 comments on commit d0ea622

Please sign in to comment.