Skip to content

Commit dc71bc0

Browse files
authored
Merge pull request microsoft#1157 from Microsoft/octogonz/ae-incompatible-tags
[api-extractor] Implement the "ae-incompatible-release-tags" warning
2 parents d21ed49 + 0818f3e commit dc71bc0

21 files changed

Lines changed: 315 additions & 27 deletions

apps/api-extractor-model/src/aedoc/ReleaseTag.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,39 @@ export enum ReleaseTag {
4343
*/
4444
Public = 4
4545
}
46+
47+
/**
48+
* Helper functions for working with the `ReleaseTag` enum.
49+
* @public
50+
*/
51+
export namespace ReleaseTag {
52+
/**
53+
* Returns the TSDoc tag name for a `ReleaseTag` value.
54+
*
55+
* @remarks
56+
* For example, `getTagName(ReleaseTag.Internal)` would return the string `@internal`.
57+
*/
58+
export function getTagName(releaseTag: ReleaseTag): string {
59+
switch (releaseTag) {
60+
case ReleaseTag.None: return '(none)';
61+
case ReleaseTag.Internal: return '@internal';
62+
case ReleaseTag.Alpha: return '@alpha';
63+
case ReleaseTag.Beta: return '@beta';
64+
case ReleaseTag.Public: return '@public';
65+
}
66+
throw new Error('Unsupported release tag');
67+
}
68+
69+
/**
70+
* Compares two `ReleaseTag` values. Their values must not be `ReleaseTag.None`.
71+
* @returns 0 if `a` and `b` are equal, a positive number if `a` is more public than `b`,
72+
* and a negative number if `a` is less public than `b`.
73+
* @remarks
74+
* For example, `compareReleaseTag(ReleaseTag.Beta, ReleaseTag.Alpha)` will return a positive
75+
* number because beta is more public than alpha.
76+
*/
77+
export function compare(a: ReleaseTag, b: ReleaseTag): number {
78+
return a - b;
79+
}
80+
81+
}

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ export const enum ExtractorMessageId {
2020
/**
2121
* This symbol has another declaration with a different release tag.
2222
*/
23-
InconsistentReleaseTags = 'ae-inconsistent-release-tags',
23+
DifferentReleaseTags = 'ae-different-release-tags',
24+
25+
/**
26+
* The symbol ___ is marked as ___, but its signature references ___ which is marked as ___.
27+
*/
28+
IncompatibleReleaseTags = 'ae-incompatible-release-tags',
2429

2530
/**
2631
* The doc comment should not contain more than one release tag.
@@ -40,7 +45,9 @@ export const enum ExtractorMessageId {
4045

4146
export const allExtractorMessageIds: Set<string> = new Set<string>([
4247
'ae-extra-release-tag',
43-
'ae-inconsistent-release-tags',
48+
'ae-different-release-tags',
49+
'ae-incompatible-release-tags',
4450
'ae-missing-release-tag',
45-
'ae-misplaced-package-tag'
51+
'ae-misplaced-package-tag',
52+
'ae-forgotten-export'
4653
]);

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ export class Collector {
493493
if (effectiveReleaseTag !== ReleaseTag.None && effectiveReleaseTag !== declaredReleaseTag) {
494494
if (!astSymbol.isExternal) { // for now, don't report errors for external code
495495
this.messageRouter.addAnalyzerIssue(
496-
ExtractorMessageId.InconsistentReleaseTags,
496+
ExtractorMessageId.DifferentReleaseTags,
497497
'This symbol has another declaration with a different release tag',
498498
astDeclaration
499499
);
@@ -553,34 +553,34 @@ export class Collector {
553553
const modifierTagSet: tsdoc.StandardModifierTagSet = parserContext.docComment.modifierTagSet;
554554

555555
let declaredReleaseTag: ReleaseTag = ReleaseTag.None;
556-
let inconsistentReleaseTags: boolean = false;
556+
let extraReleaseTags: boolean = false;
557557

558558
if (modifierTagSet.isPublic()) {
559559
declaredReleaseTag = ReleaseTag.Public;
560560
}
561561
if (modifierTagSet.isBeta()) {
562562
if (declaredReleaseTag !== ReleaseTag.None) {
563-
inconsistentReleaseTags = true;
563+
extraReleaseTags = true;
564564
} else {
565565
declaredReleaseTag = ReleaseTag.Beta;
566566
}
567567
}
568568
if (modifierTagSet.isAlpha()) {
569569
if (declaredReleaseTag !== ReleaseTag.None) {
570-
inconsistentReleaseTags = true;
570+
extraReleaseTags = true;
571571
} else {
572572
declaredReleaseTag = ReleaseTag.Alpha;
573573
}
574574
}
575575
if (modifierTagSet.isInternal()) {
576576
if (declaredReleaseTag !== ReleaseTag.None) {
577-
inconsistentReleaseTags = true;
577+
extraReleaseTags = true;
578578
} else {
579579
declaredReleaseTag = ReleaseTag.Internal;
580580
}
581581
}
582582

583-
if (inconsistentReleaseTags) {
583+
if (extraReleaseTags) {
584584
if (!astDeclaration.astSymbol.isExternal) { // for now, don't report errors for external code
585585
this.messageRouter.addAnalyzerIssue(
586586
ExtractorMessageId.ExtraReleaseTag,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class MessageRouter {
8888
} else if (!/^ae-/.test(messageId)) {
8989
throw new Error(`Error in API Extractor config: The messages.extractorMessageReporting table contains`
9090
+ ` an invalid entry "${messageId}". The name should begin with the "ae-" prefix.`);
91-
} else if (allExtractorMessageIds.has(messageId)) {
91+
} else if (!allExtractorMessageIds.has(messageId)) {
9292
throw new Error(`Error in API Extractor config: The messages.extractorMessageReporting table contains`
9393
+ ` an unrecognized identifier "${messageId}". Is it spelled correctly?`);
9494
} else {

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import * as ts from 'typescript';
44
import { Collector } from './Collector';
55
import { AstSymbol } from '../analyzer/AstSymbol';
66
import { AstDeclaration } from '../analyzer/AstDeclaration';
7-
// import { SymbolMetadata } from './SymbolMetadata';
7+
import { SymbolMetadata } from './SymbolMetadata';
88
import { CollectorEntity } from './CollectorEntity';
99
import { ExtractorMessageId } from '../api/ExtractorMessageId';
10+
import { ReleaseTag } from '@microsoft/api-extractor-model';
1011

1112
export class VisibilityChecker {
1213

@@ -28,6 +29,9 @@ export class VisibilityChecker {
2829
private static _checkReferences(collector: Collector, astDeclaration: AstDeclaration,
2930
alreadyWarnedSymbols: Set<AstSymbol>): void {
3031

32+
const astSymbolMetadata: SymbolMetadata = collector.fetchMetadata(astDeclaration.astSymbol);
33+
const astSymbolReleaseTag: ReleaseTag = astSymbolMetadata.releaseTag;
34+
3135
for (const referencedEntity of astDeclaration.referencedAstEntities) {
3236

3337
if (referencedEntity instanceof AstSymbol) {
@@ -41,8 +45,17 @@ export class VisibilityChecker {
4145
const collectorEntity: CollectorEntity | undefined = collector.tryGetCollectorEntity(rootSymbol);
4246

4347
if (collectorEntity && collectorEntity.exported) {
44-
// const metadata: SymbolMetadata = collector.fetchMetadata(referencedEntity);
45-
return;
48+
const referencedMetadata: SymbolMetadata = collector.fetchMetadata(referencedEntity);
49+
const referencedReleaseTag: ReleaseTag = referencedMetadata.releaseTag;
50+
51+
if (ReleaseTag.compare(astSymbolReleaseTag, referencedReleaseTag) > 0) {
52+
collector.messageRouter.addAnalyzerIssue(ExtractorMessageId.IncompatibleReleaseTags,
53+
`The symbol "${astDeclaration.astSymbol.localName}"`
54+
+ ` is marked as ${ReleaseTag.getTagName(astSymbolReleaseTag)},`
55+
+ ` but its signature references "${referencedEntity.localName}"`
56+
+ ` which is marked as ${ReleaseTag.getTagName(referencedReleaseTag)}`,
57+
astDeclaration);
58+
}
4659
} else {
4760
const entryPointFilename: string = path.basename(collector.workingPackage.entryPointSourceFile.fileName);
4861

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

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -245,19 +245,8 @@ export class ReviewFileGenerator {
245245
const footerParts: string[] = [];
246246

247247
if (!symbolMetadata.releaseTagSameAsParent) {
248-
switch (symbolMetadata.releaseTag) {
249-
case ReleaseTag.Internal:
250-
footerParts.push('@internal');
251-
break;
252-
case ReleaseTag.Alpha:
253-
footerParts.push('@alpha');
254-
break;
255-
case ReleaseTag.Beta:
256-
footerParts.push('@beta');
257-
break;
258-
case ReleaseTag.Public:
259-
footerParts.push('@public');
260-
break;
248+
if (symbolMetadata.releaseTag !== ReleaseTag.None) {
249+
footerParts.push(ReleaseTag.getTagName(symbolMetadata.releaseTag));
261250
}
262251
}
263252

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
"ae-forgotten-export": {
5656
"logLevel": "warning",
5757
"addToApiReviewFile": true
58+
},
59+
"ae-incompatible-release-tags": {
60+
"logLevel": "warning",
61+
"addToApiReviewFile": true
5862
}
5963
},
6064
"tsdocMessageReporting": {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"exportStar2",
1717
"exportStar3",
1818
"importEquals",
19+
"inconsistentReleaseTags",
1920
"typeOf"
2021
]
2122
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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": "(IBeta:interface)",
20+
"docComment": "/**\n * @beta\n */\n",
21+
"excerptTokens": [
22+
{
23+
"kind": "Content",
24+
"text": "export interface "
25+
},
26+
{
27+
"kind": "Reference",
28+
"text": "IBeta"
29+
},
30+
{
31+
"kind": "Content",
32+
"text": " "
33+
}
34+
],
35+
"releaseTag": "Beta",
36+
"name": "IBeta",
37+
"members": [
38+
{
39+
"kind": "PropertySignature",
40+
"canonicalReference": "x",
41+
"docComment": "",
42+
"excerptTokens": [
43+
{
44+
"kind": "Reference",
45+
"text": "x"
46+
},
47+
{
48+
"kind": "Content",
49+
"text": ": "
50+
},
51+
{
52+
"kind": "Content",
53+
"text": "number"
54+
},
55+
{
56+
"kind": "Content",
57+
"text": ";"
58+
}
59+
],
60+
"releaseTag": "Beta",
61+
"name": "x",
62+
"propertyTypeTokenRange": {
63+
"startIndex": 2,
64+
"endIndex": 3
65+
}
66+
}
67+
],
68+
"extendsTokenRanges": []
69+
},
70+
{
71+
"kind": "Function",
72+
"canonicalReference": "(publicFunctionReturnsBeta:0)",
73+
"docComment": "/**\n * It's not okay for a \"public\" function to reference a \"beta\" symbol, because \"beta\" is less public than \"public\".\n *\n * @public\n */\n",
74+
"excerptTokens": [
75+
{
76+
"kind": "Content",
77+
"text": "export declare function "
78+
},
79+
{
80+
"kind": "Reference",
81+
"text": "publicFunctionReturnsBeta"
82+
},
83+
{
84+
"kind": "Content",
85+
"text": "(): "
86+
},
87+
{
88+
"kind": "Reference",
89+
"text": "IBeta"
90+
},
91+
{
92+
"kind": "Content",
93+
"text": ";"
94+
}
95+
],
96+
"returnTypeTokenRange": {
97+
"startIndex": 3,
98+
"endIndex": 4
99+
},
100+
"releaseTag": "Public",
101+
"overloadIndex": 0,
102+
"parameters": [],
103+
"name": "publicFunctionReturnsBeta"
104+
}
105+
]
106+
}
107+
]
108+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
// @alpha
8+
declare function alphaFunctionReturnsBeta(): IBeta;
9+
10+
// @beta (undocumented)
11+
interface IBeta {
12+
// (undocumented)
13+
x: number;
14+
}
15+
16+
// Warning: (ae-incompatible-release-tags) The symbol "publicFunctionReturnsBeta" is marked as @public, but its signature references "IBeta" which is marked as @beta
17+
//
18+
// @public
19+
declare function publicFunctionReturnsBeta(): IBeta;
20+
21+
22+
// (No @packageDocumentation comment for this package)
23+
24+
```

0 commit comments

Comments
 (0)