-
Notifications
You must be signed in to change notification settings - Fork 609
/
Copy pathAstReferenceResolver.ts
300 lines (259 loc) · 10.5 KB
/
AstReferenceResolver.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as ts from 'typescript';
import * as tsdoc from '@microsoft/tsdoc';
import type { AstSymbolTable } from './AstSymbolTable';
import type { AstEntity } from './AstEntity';
import type { AstDeclaration } from './AstDeclaration';
import type { WorkingPackage } from '../collector/WorkingPackage';
import type { AstModule } from './AstModule';
import type { Collector } from '../collector/Collector';
import type { DeclarationMetadata } from '../collector/DeclarationMetadata';
import { AstSymbol } from './AstSymbol';
/**
* Used by `AstReferenceResolver` to report a failed resolution.
*
* @privateRemarks
* This class is similar to an `Error` object, but the intent of `ResolverFailure` is to describe
* why a reference could not be resolved. This information could be used to throw an actual `Error` object,
* but normally it is handed off to the `MessageRouter` instead.
*/
export class ResolverFailure {
/**
* Details about why the failure occurred.
*/
public readonly reason: string;
public constructor(reason: string) {
this.reason = reason;
}
}
/**
* This resolves a TSDoc declaration reference by walking the `AstSymbolTable` compiler state.
*
* @remarks
*
* This class is analogous to `ModelReferenceResolver` from the `@microsoft/api-extractor-model` project,
* which resolves declaration references by walking the hierarchy loaded from an .api.json file.
*/
export class AstReferenceResolver {
private readonly _collector: Collector;
private readonly _astSymbolTable: AstSymbolTable;
private readonly _workingPackage: WorkingPackage;
public constructor(collector: Collector) {
this._collector = collector;
this._astSymbolTable = collector.astSymbolTable;
this._workingPackage = collector.workingPackage;
}
public resolve(declarationReference: tsdoc.DocDeclarationReference): AstDeclaration | ResolverFailure {
// Is it referring to the working package?
if (
declarationReference.packageName !== undefined &&
declarationReference.packageName !== this._workingPackage.name
) {
return new ResolverFailure('External package references are not supported');
}
// Is it a path-based import?
if (declarationReference.importPath) {
return new ResolverFailure('Import paths are not supported');
}
const astModule: AstModule = this._astSymbolTable.fetchAstModuleFromWorkingPackage(
this._workingPackage.entryPointSourceFile
);
if (declarationReference.memberReferences.length === 0) {
return new ResolverFailure('Package references are not supported');
}
const rootMemberReference: tsdoc.DocMemberReference = declarationReference.memberReferences[0];
const exportName: string | ResolverFailure = this._getMemberReferenceIdentifier(rootMemberReference);
if (exportName instanceof ResolverFailure) {
return exportName;
}
const rootAstEntity: AstEntity | undefined = this._astSymbolTable.tryGetExportOfAstModule(
exportName,
astModule
);
if (rootAstEntity === undefined) {
return new ResolverFailure(
`The package "${this._workingPackage.name}" does not have an export "${exportName}"`
);
}
if (!(rootAstEntity instanceof AstSymbol)) {
return new ResolverFailure('This type of declaration is not supported yet by the resolver');
}
let currentDeclaration: AstDeclaration | ResolverFailure = this._selectDeclaration(
rootAstEntity.astDeclarations,
rootMemberReference,
rootAstEntity.localName
);
if (currentDeclaration instanceof ResolverFailure) {
return currentDeclaration;
}
for (let index: number = 1; index < declarationReference.memberReferences.length; ++index) {
const memberReference: tsdoc.DocMemberReference = declarationReference.memberReferences[index];
const memberName: string | ResolverFailure = this._getMemberReferenceIdentifier(memberReference);
if (memberName instanceof ResolverFailure) {
return memberName;
}
const matchingChildren: ReadonlyArray<AstDeclaration> =
currentDeclaration.findChildrenWithName(memberName);
if (matchingChildren.length === 0) {
return new ResolverFailure(`No member was found with name "${memberName}"`);
}
const selectedDeclaration: AstDeclaration | ResolverFailure = this._selectDeclaration(
matchingChildren,
memberReference,
memberName
);
if (selectedDeclaration instanceof ResolverFailure) {
return selectedDeclaration;
}
currentDeclaration = selectedDeclaration;
}
return currentDeclaration;
}
private _getMemberReferenceIdentifier(memberReference: tsdoc.DocMemberReference): string | ResolverFailure {
if (memberReference.memberSymbol !== undefined) {
return new ResolverFailure('ECMAScript symbol selectors are not supported');
}
if (memberReference.memberIdentifier === undefined) {
return new ResolverFailure('The member identifier is missing in the root member reference');
}
return memberReference.memberIdentifier.identifier;
}
private _selectDeclaration(
astDeclarations: ReadonlyArray<AstDeclaration>,
memberReference: tsdoc.DocMemberReference,
astSymbolName: string
): AstDeclaration | ResolverFailure {
const memberSelector: tsdoc.DocMemberSelector | undefined = memberReference.selector;
if (memberSelector === undefined) {
if (astDeclarations.length === 1) {
return astDeclarations[0];
} else {
// If we found multiple matches, but the extra ones are all ancillary declarations,
// then return the main declaration.
const nonAncillaryMatch: AstDeclaration | undefined =
this._tryDisambiguateAncillaryMatches(astDeclarations);
if (nonAncillaryMatch) {
return nonAncillaryMatch;
}
return new ResolverFailure(
`The reference is ambiguous because "${astSymbolName}"` +
` has more than one declaration; you need to add a TSDoc member reference selector`
);
}
}
switch (memberSelector.selectorKind) {
case tsdoc.SelectorKind.System:
return this._selectUsingSystemSelector(astDeclarations, memberSelector, astSymbolName);
case tsdoc.SelectorKind.Index:
return this._selectUsingIndexSelector(astDeclarations, memberSelector, astSymbolName);
}
return new ResolverFailure(`The selector "${memberSelector.selector}" is not a supported selector type`);
}
private _selectUsingSystemSelector(
astDeclarations: ReadonlyArray<AstDeclaration>,
memberSelector: tsdoc.DocMemberSelector,
astSymbolName: string
): AstDeclaration | ResolverFailure {
const selectorName: string = memberSelector.selector;
let selectorSyntaxKind: ts.SyntaxKind;
switch (selectorName) {
case 'class':
selectorSyntaxKind = ts.SyntaxKind.ClassDeclaration;
break;
case 'enum':
selectorSyntaxKind = ts.SyntaxKind.EnumDeclaration;
break;
case 'function':
selectorSyntaxKind = ts.SyntaxKind.FunctionDeclaration;
break;
case 'interface':
selectorSyntaxKind = ts.SyntaxKind.InterfaceDeclaration;
break;
case 'namespace':
selectorSyntaxKind = ts.SyntaxKind.ModuleDeclaration;
break;
case 'type':
selectorSyntaxKind = ts.SyntaxKind.TypeAliasDeclaration;
break;
case 'variable':
selectorSyntaxKind = ts.SyntaxKind.VariableDeclaration;
break;
default:
return new ResolverFailure(`Unsupported system selector "${selectorName}"`);
}
const matches: AstDeclaration[] = astDeclarations.filter(
(x) => x.declaration.kind === selectorSyntaxKind
);
if (matches.length === 0) {
return new ResolverFailure(
`A declaration for "${astSymbolName}" was not found that matches the` +
` TSDoc selector "${selectorName}"`
);
}
if (matches.length > 1) {
// If we found multiple matches, but the extra ones are all ancillary declarations,
// then return the main declaration.
const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(matches);
if (nonAncillaryMatch) {
return nonAncillaryMatch;
}
return new ResolverFailure(
`More than one declaration "${astSymbolName}" matches the TSDoc selector "${selectorName}"`
);
}
return matches[0];
}
private _selectUsingIndexSelector(
astDeclarations: ReadonlyArray<AstDeclaration>,
memberSelector: tsdoc.DocMemberSelector,
astSymbolName: string
): AstDeclaration | ResolverFailure {
const selectorOverloadIndex: number = parseInt(memberSelector.selector, 10);
const matches: AstDeclaration[] = [];
for (const astDeclaration of astDeclarations) {
const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration);
if (overloadIndex === selectorOverloadIndex) {
matches.push(astDeclaration);
}
}
if (matches.length === 0) {
return new ResolverFailure(
`An overload for "${astSymbolName}" was not found that matches the` +
` TSDoc selector ":${selectorOverloadIndex}"`
);
}
if (matches.length > 1) {
// If we found multiple matches, but the extra ones are all ancillary declarations,
// then return the main declaration.
const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(matches);
if (nonAncillaryMatch) {
return nonAncillaryMatch;
}
return new ResolverFailure(
`More than one declaration for "${astSymbolName}" matches the` +
` TSDoc selector ":${selectorOverloadIndex}"`
);
}
return matches[0];
}
/**
* This resolves an ambiguous match in the case where the extra matches are all ancillary declarations,
* except for one match that is the main declaration.
*/
private _tryDisambiguateAncillaryMatches(
matches: ReadonlyArray<AstDeclaration>
): AstDeclaration | undefined {
let result: AstDeclaration | undefined = undefined;
for (const match of matches) {
const declarationMetadata: DeclarationMetadata = this._collector.fetchDeclarationMetadata(match);
if (!declarationMetadata.isAncillary) {
if (result) {
return undefined; // more than one match
}
result = match;
}
}
return result;
}
}