Skip to content

Commit eade95f

Browse files
authored
Merge pull request microsoft#1173 from Microsoft/octogonz/ae-more-validation
[api-extractor] Miscellaneous validation improvements
2 parents 6218332 + 0a28c9a commit eade95f

16 files changed

Lines changed: 271 additions & 27 deletions

File tree

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,49 @@
1313
*/
1414
export const enum ExtractorMessageId {
1515
/**
16-
* The doc comment should not contain more than one release tag.
16+
* "The doc comment should not contain more than one release tag."
1717
*/
1818
ExtraReleaseTag = 'ae-extra-release-tag',
1919

2020
/**
21-
* This symbol has another declaration with a different release tag.
21+
* "This symbol has another declaration with a different release tag."
2222
*/
2323
DifferentReleaseTags = 'ae-different-release-tags',
2424

2525
/**
26-
* The symbol ___ is marked as ___, but its signature references ___ which is marked as ___.
26+
* "The symbol ___ is marked as ___, but its signature references ___ which is marked as ___."
2727
*/
2828
IncompatibleReleaseTags = 'ae-incompatible-release-tags',
2929

3030
/**
31-
* The doc comment should not contain more than one release tag.
31+
* "___ is exported by the package, but it is missing a release tag (`@alpha`, `@beta`, `@public`, or `@internal`)."
3232
*/
3333
MissingReleaseTag = 'ae-missing-release-tag',
3434

3535
/**
36-
* The `@packageDocumentation` comment must appear at the top of entry point *.d.ts file.
36+
* "The `@packageDocumentation` comment must appear at the top of entry point *.d.ts file."
3737
*/
3838
MisplacedPackageTag = 'ae-misplaced-package-tag',
3939

4040
/**
41-
* The symbol ___ needs to be exported by the entry point ___.
41+
* "The symbol ___ needs to be exported by the entry point ___."
4242
*/
4343
ForgottenExport = 'ae-forgotten-export',
4444

4545
/**
46-
* The name ___ should be prefixed with an underscore because the declaration is marked as `@internal`.
46+
* "The name ___ should be prefixed with an underscore because the declaration is marked as `@internal`."
4747
*/
48-
InternalMissingUnderscore = 'ae-internal-missing-underscore'
48+
InternalMissingUnderscore = 'ae-internal-missing-underscore',
49+
50+
/**
51+
* "The `@preapproved` tag cannot be applied to ___ because it is not a supported declaration type."
52+
*/
53+
PreapprovedUnsupportedType = 'ae-preapproved-unsupported-type',
54+
55+
/**
56+
* "The `@preapproved` tag cannot be applied to ___ without an `@internal` release tag."
57+
*/
58+
PreapprovedBadReleaseTag = 'ae-preapproved-bad-release-tag'
4959
}
5060

5161
export const allExtractorMessageIds: Set<string> = new Set<string>([

apps/api-extractor/src/collector/Collector.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,8 @@ export class Collector {
523523

524524
this.messageRouter.addAnalyzerIssue(
525525
ExtractorMessageId.MissingReleaseTag,
526-
'Missing release tag',
526+
`"${entity.nameForEmit}" is exported by the package, but it is missing `
527+
+ `a release tag (@alpha, @beta, @public, or @internal)`,
527528
astSymbol
528529
);
529530
}
@@ -599,9 +600,43 @@ export class Collector {
599600
declarationMetadata.isSealed = modifierTagSet.isSealed();
600601
declarationMetadata.isVirtual = modifierTagSet.isVirtual();
601602

602-
// Require the summary to contain at least 10 non-spacing characters
603-
declarationMetadata.needsDocumentation = !tsdoc.PlainTextEmitter.hasAnyTextContent(
604-
parserContext.docComment.summarySection, 10);
603+
if (modifierTagSet.hasTag(AedocDefinitions.preapprovedTag)) {
604+
// This feature only makes sense for potentially big declarations.
605+
switch (astDeclaration.declaration.kind) {
606+
case ts.SyntaxKind.ClassDeclaration:
607+
case ts.SyntaxKind.EnumDeclaration:
608+
case ts.SyntaxKind.InterfaceDeclaration:
609+
case ts.SyntaxKind.ModuleDeclaration:
610+
if (declaredReleaseTag === ReleaseTag.Internal) {
611+
declarationMetadata.isPreapproved = true;
612+
} else {
613+
this.messageRouter.addAnalyzerIssue(
614+
ExtractorMessageId.PreapprovedBadReleaseTag,
615+
`The @preapproved tag cannot be applied to "${astDeclaration.astSymbol.localName}"`
616+
+ ` without an @internal release tag`,
617+
astDeclaration
618+
);
619+
}
620+
break;
621+
default:
622+
this.messageRouter.addAnalyzerIssue(
623+
ExtractorMessageId.PreapprovedUnsupportedType,
624+
`The @preapproved tag cannot be applied to "${astDeclaration.astSymbol.localName}"`
625+
+ ` because it is not a supported declaration type`,
626+
astDeclaration
627+
);
628+
break;
629+
}
630+
}
631+
632+
if (astDeclaration.declaration.kind === ts.SyntaxKind.Constructor) {
633+
// NOTE: If the constructor summary is missing, then ApiModelGenerator will auto-generate one.
634+
declarationMetadata.needsDocumentation = false;
635+
} else {
636+
// Require the summary to contain at least 10 non-spacing characters
637+
declarationMetadata.needsDocumentation = !tsdoc.PlainTextEmitter.hasAnyTextContent(
638+
parserContext.docComment.summarySection, 10);
639+
}
605640
}
606641
}
607642

apps/api-extractor/src/collector/DeclarationMetadata.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ export class DeclarationMetadata {
2222
public isSealed: boolean = false;
2323
public isVirtual: boolean = false;
2424

25+
public isPreapproved: boolean = false;
26+
2527
public needsDocumentation: boolean = true;
2628
}

apps/api-extractor/src/generators/ApiModelGenerator.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
ApiIndexSignature,
3030
ApiVariable,
3131
ApiTypeAlias,
32-
ApiCallSignature
32+
ApiCallSignature,
33+
AedocDefinitions
3334
} from '@microsoft/api-extractor-model';
3435

3536
import { Collector } from '../collector/Collector';
@@ -226,9 +227,28 @@ export class ApiModelGenerator {
226227
nodesToCapture
227228
});
228229

229-
const docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment;
230+
let docComment: tsdoc.DocComment | undefined = this._collector.fetchMetadata(astDeclaration).tsdocComment;
230231
const releaseTag: ReleaseTag = this._collector.fetchMetadata(astDeclaration.astSymbol).releaseTag;
231232

233+
// Constructors always do pretty much the same thing, so it's annoying to require people to write
234+
// descriptions for them. Instead, if the constructor lacks a TSDoc summary, then API Extractor
235+
// will auto-generate one.
236+
const configuration: tsdoc.TSDocConfiguration = AedocDefinitions.tsdocConfiguration;
237+
if (docComment === undefined) {
238+
docComment = new tsdoc.DocComment({ configuration });
239+
}
240+
241+
if (!tsdoc.PlainTextEmitter.hasAnyTextContent(docComment.summarySection)) {
242+
docComment.summarySection.appendNodesInParagraph([
243+
new tsdoc.DocPlainText({ configuration, text: 'Constructs a new instance of the ' }),
244+
new tsdoc.DocCodeSpan({
245+
configuration,
246+
code: parentApiItem.displayName
247+
}),
248+
new tsdoc.DocPlainText({ configuration, text: ' class' })
249+
]);
250+
}
251+
232252
apiConstructor = new ApiConstructor({ docComment, releaseTag, isStatic, parameters, overloadIndex,
233253
excerptTokens });
234254

apps/api-extractor/src/generators/ReviewFileGenerator.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ export class ReviewFileGenerator {
104104
stringWriter.write(ReviewFileGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport));
105105

106106
const span: Span = new Span(astDeclaration.declaration);
107-
ReviewFileGenerator._modifySpan(collector, span, entity, astDeclaration, false);
107+
108+
const declarationMetadata: DeclarationMetadata = collector.fetchMetadata(astDeclaration);
109+
if (declarationMetadata.isPreapproved) {
110+
ReviewFileGenerator._modifySpanForPreapproved(span);
111+
} else {
112+
ReviewFileGenerator._modifySpan(collector, span, entity, astDeclaration, false);
113+
}
114+
108115
span.writeModifiedText(stringWriter.stringBuilder);
109116
stringWriter.writeLine('\n');
110117
}
@@ -301,6 +308,57 @@ export class ReviewFileGenerator {
301308
}
302309
}
303310

311+
/**
312+
* For declarations marked as `@preapproved`, this is used instead of _modifySpan().
313+
*/
314+
private static _modifySpanForPreapproved(span: Span): void {
315+
// Match something like this:
316+
//
317+
// ClassDeclaration:
318+
// SyntaxList:
319+
// ExportKeyword: pre=[export] sep=[ ]
320+
// DeclareKeyword: pre=[declare] sep=[ ]
321+
// ClassKeyword: pre=[class] sep=[ ]
322+
// Identifier: pre=[_PreapprovedClass] sep=[ ]
323+
// FirstPunctuation: pre=[{] sep=[\n\n ]
324+
// SyntaxList:
325+
// ...
326+
// CloseBraceToken: pre=[}]
327+
//
328+
// or this:
329+
// ModuleDeclaration:
330+
// SyntaxList:
331+
// ExportKeyword: pre=[export] sep=[ ]
332+
// DeclareKeyword: pre=[declare] sep=[ ]
333+
// NamespaceKeyword: pre=[namespace] sep=[ ]
334+
// Identifier: pre=[_PreapprovedNamespace] sep=[ ]
335+
// ModuleBlock:
336+
// FirstPunctuation: pre=[{] sep=[\n\n ]
337+
// SyntaxList:
338+
// ...
339+
// CloseBraceToken: pre=[}]
340+
//
341+
// And reduce it to something like this:
342+
//
343+
// // @internal (undocumented)
344+
// class _PreapprovedClass { /* (preapproved) */ }
345+
//
346+
347+
let skipRest: boolean = false;
348+
for (const child of span.children) {
349+
if (skipRest
350+
|| child.kind === ts.SyntaxKind.SyntaxList
351+
|| child.kind === ts.SyntaxKind.JSDocComment) {
352+
child.modification.skipAll();
353+
}
354+
if (child.kind === ts.SyntaxKind.Identifier) {
355+
skipRest = true;
356+
child.modification.omitSeparatorAfter = true;
357+
child.modification.suffix = ' { /* (preapproved) */ }';
358+
}
359+
}
360+
}
361+
304362
/**
305363
* Writes a synopsis of the AEDoc comments, which indicates the release tag,
306364
* whether the item has been documented, and any warnings that were detected

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"exportStar3",
1818
"importEquals",
1919
"inconsistentReleaseTags",
20+
"preapproved",
2021
"typeOf"
2122
]
2223
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
]
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
// @internal (undocumented)
8+
class _PreapprovedClass { /* (preapproved) */ }
9+
10+
// @internal (undocumented)
11+
enum _PreapprovedEnum { /* (preapproved) */ }
12+
13+
// @internal (undocumented)
14+
interface _PreapprovedInterface { /* (preapproved) */ }
15+
16+
// @internal (undocumented)
17+
namespace _PreapprovedNamespace { /* (preapproved) */ }
18+
19+
20+
// (No @packageDocumentation comment for this package)
21+
22+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
/** @internal @preapproved */
3+
export declare class _PreapprovedClass {
4+
member(): void;
5+
}
6+
7+
/** @internal @preapproved */
8+
export declare enum _PreapprovedEnum {
9+
ONE = 1,
10+
TWO = 2
11+
}
12+
13+
/** @internal @preapproved */
14+
export declare interface _PreapprovedInterface {
15+
member(): void;
16+
}
17+
18+
/** @internal @preapproved */
19+
export declare namespace _PreapprovedNamespace {
20+
export class X {
21+
}
22+
export function f(): void;
23+
}
24+
25+
export { }

build-tests/api-extractor-scenarios/src/apiItemKinds/classes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ export abstract class AbstractClass {
1111
export class SimpleClass {
1212
public member(): void {
1313
}
14-
}
14+
}

0 commit comments

Comments
 (0)