Skip to content

Commit 40d84f6

Browse files
committed
Implement the "ae-unresolved-link" check
1 parent 8390b24 commit 40d84f6

File tree

9 files changed

+353
-3
lines changed

9 files changed

+353
-3
lines changed

apps/api-extractor/src/api/ExtractorMessageId.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ export const enum ExtractorMessageId {
7575
/**
7676
* "The `@inheritDoc` tag for ____ refers to its own declaration".
7777
*/
78-
CyclicInheritDoc = 'ae-cyclic-inherit-doc'
78+
CyclicInheritDoc = 'ae-cyclic-inherit-doc',
79+
80+
/**
81+
* "The `@link` reference could not be resolved".
82+
*/
83+
UnresolvedLink = 'ae-unresolved-link'
7984
}
8085

8186
export const allExtractorMessageIds: Set<string> = new Set<string>([
@@ -90,5 +95,6 @@ export const allExtractorMessageIds: Set<string> = new Set<string>([
9095
'ae-preapproved-bad-release-tag',
9196
'ae-unresolved-inheritdoc-reference',
9297
'ae-unresolved-inheritdoc-base',
93-
'ae-cyclic-inherit-doc'
98+
'ae-cyclic-inherit-doc',
99+
'ae-unresolved-link'
94100
]);

apps/api-extractor/src/enhancers/DocCommentEnhancer.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export class DocCommentEnhancer {
5959

6060
this._analyzeNeedsDocumentation(astDeclaration, metadata);
6161

62+
this._checkForBrokenLinks(astDeclaration, metadata);
63+
6264
metadata.docCommentEnhancerVisitorState = VisitorState.Visited;
6365
}
6466

@@ -96,6 +98,32 @@ export class DocCommentEnhancer {
9698
}
9799
}
98100

101+
private _checkForBrokenLinks(astDeclaration: AstDeclaration, metadata: DeclarationMetadata): void {
102+
if (!metadata.tsdocComment) {
103+
return;
104+
}
105+
this._checkForBrokenLinksRecursive(astDeclaration, metadata.tsdocComment);
106+
}
107+
108+
private _checkForBrokenLinksRecursive(astDeclaration: AstDeclaration, node: tsdoc.DocNode): void {
109+
if (node instanceof tsdoc.DocLinkTag) {
110+
if (node.codeDestination) {
111+
const referencedAstDeclaration: AstDeclaration | ResolverFailure = this._collector.astReferenceResolver
112+
.resolve(node.codeDestination);
113+
114+
if (referencedAstDeclaration instanceof ResolverFailure) {
115+
this._collector.messageRouter.addAnalyzerIssue(ExtractorMessageId.UnresolvedLink,
116+
'The @link reference could not be resolved: ' + referencedAstDeclaration.reason,
117+
astDeclaration);
118+
}
119+
120+
}
121+
}
122+
for (const childNode of node.getChildNodes()) {
123+
this._checkForBrokenLinksRecursive(astDeclaration, childNode);
124+
}
125+
}
126+
99127
/**
100128
* Follow an `{@inheritDoc ___}` reference and copy the content that we find in the referenced comment.
101129
*/

apps/api-extractor/src/schemas/api-extractor-defaults.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@
7171
"ae-unresolved-inheritdoc-base": {
7272
"logLevel": "warning",
7373
"addToApiReviewFile": true
74+
},
75+
"ae-unresolved-link": {
76+
"logLevel": "warning",
77+
"addToApiReviewFile": true
7478
}
7579
},
7680
"tsdocMessageReporting": {

build-tests/api-extractor-scenarios/config/build-config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"defaultExportOfEntryPoint4",
1111
"docReferences",
1212
"docReferences2",
13+
"docReferences3",
1314
"exportDuplicate",
1415
"exportEquals",
1516
"exportImportedExternal",
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
{
2+
"metadata": {
3+
"toolPackage": "@microsoft/api-extractor",
4+
"toolVersion": "[test mode]",
5+
"schemaVersion": 1000
6+
},
7+
"kind": "Package",
8+
"canonicalReference": "api-extractor-scenarios",
9+
"docComment": "",
10+
"name": "api-extractor-scenarios",
11+
"members": [
12+
{
13+
"kind": "EntryPoint",
14+
"canonicalReference": "",
15+
"name": "",
16+
"members": [
17+
{
18+
"kind": "Interface",
19+
"canonicalReference": "(A:interface)",
20+
"docComment": "",
21+
"excerptTokens": [
22+
{
23+
"kind": "Content",
24+
"text": "export interface "
25+
},
26+
{
27+
"kind": "Reference",
28+
"text": "A"
29+
},
30+
{
31+
"kind": "Content",
32+
"text": " "
33+
}
34+
],
35+
"releaseTag": "Public",
36+
"name": "A",
37+
"members": [
38+
{
39+
"kind": "PropertySignature",
40+
"canonicalReference": "myProperty",
41+
"docComment": "",
42+
"excerptTokens": [
43+
{
44+
"kind": "Reference",
45+
"text": "myProperty"
46+
},
47+
{
48+
"kind": "Content",
49+
"text": ": "
50+
},
51+
{
52+
"kind": "Content",
53+
"text": "string"
54+
},
55+
{
56+
"kind": "Content",
57+
"text": ";"
58+
}
59+
],
60+
"releaseTag": "Public",
61+
"name": "myProperty",
62+
"propertyTypeTokenRange": {
63+
"startIndex": 2,
64+
"endIndex": 3
65+
}
66+
}
67+
],
68+
"extendsTokenRanges": []
69+
},
70+
{
71+
"kind": "Namespace",
72+
"canonicalReference": "(A:namespace)",
73+
"docComment": "/**\n * @public\n */\n",
74+
"excerptTokens": [
75+
{
76+
"kind": "Content",
77+
"text": "export declare namespace "
78+
},
79+
{
80+
"kind": "Reference",
81+
"text": "A"
82+
},
83+
{
84+
"kind": "Content",
85+
"text": " "
86+
}
87+
],
88+
"releaseTag": "Public",
89+
"name": "A",
90+
"members": [
91+
{
92+
"kind": "Class",
93+
"canonicalReference": "(B:class)",
94+
"docComment": "",
95+
"excerptTokens": [
96+
{
97+
"kind": "Content",
98+
"text": "class "
99+
},
100+
{
101+
"kind": "Reference",
102+
"text": "B"
103+
},
104+
{
105+
"kind": "Content",
106+
"text": " "
107+
}
108+
],
109+
"releaseTag": "Public",
110+
"name": "B",
111+
"members": [
112+
{
113+
"kind": "Method",
114+
"canonicalReference": "(myMethod:instance,0)",
115+
"docComment": "",
116+
"excerptTokens": [
117+
{
118+
"kind": "Reference",
119+
"text": "myMethod"
120+
},
121+
{
122+
"kind": "Content",
123+
"text": "(): "
124+
},
125+
{
126+
"kind": "Content",
127+
"text": "void"
128+
},
129+
{
130+
"kind": "Content",
131+
"text": ";"
132+
}
133+
],
134+
"isStatic": false,
135+
"returnTypeTokenRange": {
136+
"startIndex": 2,
137+
"endIndex": 3
138+
},
139+
"releaseTag": "Public",
140+
"overloadIndex": 0,
141+
"parameters": [],
142+
"name": "myMethod"
143+
}
144+
],
145+
"implementsTokenRanges": []
146+
}
147+
]
148+
},
149+
{
150+
"kind": "Function",
151+
"canonicalReference": "(failWithAmbiguity:0)",
152+
"docComment": "/**\n * {@link MyNamespace.MyClass.myMethod | the method}\n *\n * @public\n */\n",
153+
"excerptTokens": [
154+
{
155+
"kind": "Content",
156+
"text": "export declare function "
157+
},
158+
{
159+
"kind": "Reference",
160+
"text": "failWithAmbiguity"
161+
},
162+
{
163+
"kind": "Content",
164+
"text": "(): "
165+
},
166+
{
167+
"kind": "Content",
168+
"text": "void"
169+
},
170+
{
171+
"kind": "Content",
172+
"text": ";"
173+
}
174+
],
175+
"returnTypeTokenRange": {
176+
"startIndex": 3,
177+
"endIndex": 4
178+
},
179+
"releaseTag": "Public",
180+
"overloadIndex": 0,
181+
"parameters": [],
182+
"name": "failWithAmbiguity"
183+
},
184+
{
185+
"kind": "Function",
186+
"canonicalReference": "(success:0)",
187+
"docComment": "/**\n * {@link (A:namespace).B.myMethod | the method} {@link (A:interface).myProperty | the property}\n *\n * @public\n */\n",
188+
"excerptTokens": [
189+
{
190+
"kind": "Content",
191+
"text": "export declare function "
192+
},
193+
{
194+
"kind": "Reference",
195+
"text": "success"
196+
},
197+
{
198+
"kind": "Content",
199+
"text": "(): "
200+
},
201+
{
202+
"kind": "Content",
203+
"text": "void"
204+
},
205+
{
206+
"kind": "Content",
207+
"text": ";"
208+
}
209+
],
210+
"returnTypeTokenRange": {
211+
"startIndex": 3,
212+
"endIndex": 4
213+
},
214+
"releaseTag": "Public",
215+
"overloadIndex": 0,
216+
"parameters": [],
217+
"name": "success"
218+
}
219+
]
220+
}
221+
]
222+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
## API Review File for "api-extractor-scenarios"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
// @public (undocumented)
8+
export namespace A {
9+
// (undocumented)
10+
export class B {
11+
// (undocumented)
12+
myMethod(): void;
13+
}
14+
}
15+
16+
// @public (undocumented)
17+
export interface A {
18+
// (undocumented)
19+
myProperty: string;
20+
}
21+
22+
// Warning: (ae-unresolved-link) The @link reference could not be resolved:The package "api-extractor-scenarios" does not have an export "MyNamespace"
23+
//
24+
// @public
25+
export function failWithAmbiguity(): void;
26+
27+
// @public
28+
export function success(): void;
29+
30+
31+
// (No @packageDocumentation comment for this package)
32+
33+
```
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
/** @public */
3+
export declare namespace A {
4+
export class B {
5+
myMethod(): void;
6+
}
7+
}
8+
9+
export declare interface A {
10+
myProperty: string;
11+
}
12+
13+
/**
14+
* {@link MyNamespace.MyClass.myMethod | the method}
15+
* @public
16+
*/
17+
export declare function failWithAmbiguity(): void;
18+
19+
/**
20+
* {@link (A:namespace).B.myMethod | the method}
21+
* {@link (A:interface).myProperty | the property}
22+
* @public
23+
*/
24+
export declare function success(): void;
25+
26+
export { }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
/** @public */
5+
export namespace A {
6+
export class B {
7+
public myMethod(): void {
8+
}
9+
}
10+
}
11+
12+
export interface A {
13+
myProperty: string;
14+
}
15+
16+
/**
17+
* {@link MyNamespace.MyClass.myMethod | the method}
18+
* @public
19+
*/
20+
export function failWithAmbiguity() {
21+
}
22+
23+
/**
24+
* {@link (A:namespace).B.myMethod | the method}
25+
* {@link (A:interface).myProperty | the property}
26+
* @public
27+
*/
28+
export function succeedWithSelector() {
29+
}

0 commit comments

Comments
 (0)