@@ -7,6 +7,7 @@ import * as ts from 'typescript';
77import { Collector } from '../collector/Collector' ;
88import { AstSymbol } from '../analyzer/AstSymbol' ;
99import { AstDeclaration } from '../analyzer/AstDeclaration' ;
10+ import { DeclarationMetadata } from '../collector/DeclarationMetadata' ;
1011import { SymbolMetadata } from '../collector/SymbolMetadata' ;
1112import { CollectorEntity } from '../collector/CollectorEntity' ;
1213import { ExtractorMessageId } from '../api/ExtractorMessageId' ;
@@ -24,18 +25,59 @@ export class ValidationEnhancer {
2425 ValidationEnhancer . _checkReferences ( collector , astDeclaration , alreadyWarnedSymbols ) ;
2526 } ) ;
2627
27- ValidationEnhancer . _checkForInternalUnderscore ( collector , entity , entity . astEntity ) ;
28+ const symbolMetadata : SymbolMetadata = collector . fetchMetadata ( entity . astEntity ) ;
29+ ValidationEnhancer . _checkForInternalUnderscore ( collector , entity , entity . astEntity , symbolMetadata ) ;
30+ ValidationEnhancer . _checkForInconsistentReleaseTags ( collector , entity . astEntity , symbolMetadata ) ;
2831 }
2932 }
3033 }
3134 }
3235
33- private static _checkForInternalUnderscore ( collector : Collector , collectorEntity : CollectorEntity ,
34- astSymbol : AstSymbol ) : void {
36+ private static _checkForInternalUnderscore (
37+ collector : Collector ,
38+ collectorEntity : CollectorEntity ,
39+ astSymbol : AstSymbol ,
40+ symbolMetadata : SymbolMetadata
41+ ) : void {
3542
36- const astSymbolMetadata : SymbolMetadata = collector . fetchMetadata ( astSymbol ) ;
43+ let needsUnderscore : boolean = false ;
3744
38- if ( astSymbolMetadata . releaseTag === ReleaseTag . Internal && ! astSymbolMetadata . releaseTagSameAsParent ) {
45+ if ( symbolMetadata . maxEffectiveReleaseTag === ReleaseTag . Internal ) {
46+ if ( ! astSymbol . parentAstSymbol ) {
47+ // If it's marked as @internal and has no parent, then it needs and underscore.
48+ // We use maxEffectiveReleaseTag because a merged declaration would NOT need an underscore in a case like this:
49+ //
50+ // /** @public */
51+ // export enum X { }
52+ //
53+ // /** @internal */
54+ // export namespace X { }
55+ //
56+ // (The above normally reports an error "ae-different-release-tags", but that may be suppressed.)
57+ needsUnderscore = true ;
58+ } else {
59+ // If it's marked as @internal and the parent isn't obviously already @internal, then it needs an underscore.
60+ //
61+ // For example, we WOULD need an underscore for a merged declaration like this:
62+ //
63+ // /** @internal */
64+ // export namespace X {
65+ // export interface _Y { }
66+ // }
67+ //
68+ // /** @public */
69+ // export class X {
70+ // /** @internal */
71+ // public static _Y(): void { } // <==== different from parent
72+ // }
73+ const parentSymbolMetadata : SymbolMetadata = collector . fetchMetadata ( astSymbol ) ;
74+ if ( parentSymbolMetadata . maxEffectiveReleaseTag > ReleaseTag . Internal ) {
75+ needsUnderscore = true ;
76+ }
77+ }
78+ }
79+
80+ if ( needsUnderscore ) {
3981 for ( const exportName of collectorEntity . exportNames ) {
4082 if ( exportName [ 0 ] !== '_' ) {
4183 collector . messageRouter . addAnalyzerIssue (
@@ -48,14 +90,80 @@ export class ValidationEnhancer {
4890 }
4991 }
5092 }
51-
5293 }
5394
54- private static _checkReferences ( collector : Collector , astDeclaration : AstDeclaration ,
55- alreadyWarnedSymbols : Set < AstSymbol > ) : void {
95+ private static _checkForInconsistentReleaseTags (
96+ collector : Collector ,
97+ astSymbol : AstSymbol ,
98+ symbolMetadata : SymbolMetadata
99+ ) : void {
100+ if ( astSymbol . isExternal ) {
101+ // For now, don't report errors for external code. If the developer cares about it, they should run
102+ // API Extractor separately on the external project
103+ return ;
104+ }
105+
106+ // Normally we will expect all release tags to be the same. Arbitrarily we choose the maxEffectiveReleaseTag
107+ // as the thing they should all match.
108+ const expectedEffectiveReleaseTag : ReleaseTag = symbolMetadata . maxEffectiveReleaseTag ;
109+
110+ // This is set to true if we find a declaration whose release tag is different from expectedEffectiveReleaseTag
111+ let mixedReleaseTags : boolean = false ;
112+
113+ // This is set to false if we find a declaration that is not a function/method overload
114+ let onlyFunctionOverloads : boolean = true ;
115+
116+ // This is set to true if we find a declaration that is @internal
117+ let anyInternalReleaseTags : boolean = false ;
118+
119+ for ( const astDeclaration of astSymbol . astDeclarations ) {
120+ const declarationMetadata : DeclarationMetadata = collector . fetchMetadata ( astDeclaration ) ;
121+ const effectiveReleaseTag : ReleaseTag = declarationMetadata . effectiveReleaseTag ;
122+
123+ switch ( astDeclaration . declaration . kind ) {
124+ case ts . SyntaxKind . FunctionDeclaration :
125+ case ts . SyntaxKind . MethodDeclaration :
126+ break ;
127+ default :
128+ onlyFunctionOverloads = false ;
129+ }
130+
131+ if ( effectiveReleaseTag !== expectedEffectiveReleaseTag ) {
132+ mixedReleaseTags = true ;
133+ }
134+
135+ if ( effectiveReleaseTag === ReleaseTag . Internal ) {
136+ anyInternalReleaseTags = true ;
137+ }
138+ }
139+
140+ if ( mixedReleaseTags ) {
141+ if ( ! onlyFunctionOverloads ) {
142+ collector . messageRouter . addAnalyzerIssue (
143+ ExtractorMessageId . DifferentReleaseTags ,
144+ 'This symbol has another declaration with a different release tag' ,
145+ astSymbol
146+ ) ;
147+ }
148+
149+ if ( anyInternalReleaseTags ) {
150+ collector . messageRouter . addAnalyzerIssue (
151+ ExtractorMessageId . InternalMixedReleaseTag ,
152+ `Mixed release tags are not allowed for "${ astSymbol . localName } " because one of its declarations` +
153+ ` is marked as @internal` ,
154+ astSymbol
155+ ) ;
156+ }
157+ }
158+ }
56159
57- const astSymbolMetadata : SymbolMetadata = collector . fetchMetadata ( astDeclaration . astSymbol ) ;
58- const astSymbolReleaseTag : ReleaseTag = astSymbolMetadata . releaseTag ;
160+ private static _checkReferences (
161+ collector : Collector ,
162+ astDeclaration : AstDeclaration ,
163+ alreadyWarnedSymbols : Set < AstSymbol >
164+ ) : void {
165+ const declarationMetadata : DeclarationMetadata = collector . fetchMetadata ( astDeclaration ) ;
166+ const declarationReleaseTag : ReleaseTag = declarationMetadata . effectiveReleaseTag ;
59167
60168 for ( const referencedEntity of astDeclaration . referencedAstEntities ) {
61169
@@ -71,12 +179,12 @@ export class ValidationEnhancer {
71179
72180 if ( collectorEntity && collectorEntity . exported ) {
73181 const referencedMetadata : SymbolMetadata = collector . fetchMetadata ( referencedEntity ) ;
74- const referencedReleaseTag : ReleaseTag = referencedMetadata . releaseTag ;
182+ const referencedReleaseTag : ReleaseTag = referencedMetadata . maxEffectiveReleaseTag ;
75183
76- if ( ReleaseTag . compare ( astSymbolReleaseTag , referencedReleaseTag ) > 0 ) {
184+ if ( ReleaseTag . compare ( declarationReleaseTag , referencedReleaseTag ) > 0 ) {
77185 collector . messageRouter . addAnalyzerIssue ( ExtractorMessageId . IncompatibleReleaseTags ,
78186 `The symbol "${ astDeclaration . astSymbol . localName } "`
79- + ` is marked as ${ ReleaseTag . getTagName ( astSymbolReleaseTag ) } ,`
187+ + ` is marked as ${ ReleaseTag . getTagName ( declarationReleaseTag ) } ,`
80188 + ` but its signature references "${ referencedEntity . localName } "`
81189 + ` which is marked as ${ ReleaseTag . getTagName ( referencedReleaseTag ) } ` ,
82190 astDeclaration ) ;
0 commit comments