Skip to content

Commit

Permalink
Merge pull request #1170 from Microsoft/octogonz/ae-internal-missing-…
Browse files Browse the repository at this point in the history
…underscore

[api-extractor] Implement "ae-internal-missing-underscore" warning
  • Loading branch information
octogonz authored Mar 21, 2019
2 parents 4b88e32 + f141c3a commit 083a292
Show file tree
Hide file tree
Showing 14 changed files with 157 additions and 50 deletions.
26 changes: 26 additions & 0 deletions apps/api-extractor/src/api/ExtractorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ import * as path from 'path';
import { ExtractorMessageId } from './ExtractorMessageId';
import { Path, Text } from '@microsoft/node-core-library';

/**
* Used by {@link ExtractorMessage.properties}.
*
* @public
*/
export interface IExtractorMessageProperties {

/**
* A declaration can have multiple names if it is exported more than once.
* If an `ExtractorMessage` applies to a specific export name, this property can indicate that.
*
* @remarks
*
* Used by {@link ExtractorMessageId.InternalMissingUnderscore}.
*/
readonly exportName?: string;
}

/**
* Specifies a category of messages for use with {@link ExtractorMessage}.
* @public
Expand Down Expand Up @@ -49,6 +67,7 @@ export interface IExtractorMessageOptions {
sourceFilePath?: string;
sourceFileLine?: number;
sourceFileColumn?: number;
properties?: IExtractorMessageProperties;
}

/**
Expand Down Expand Up @@ -90,6 +109,12 @@ export class ExtractorMessage {
*/
public readonly sourceFileColumn: number | undefined;

/**
* Additional contextual information about the message that may be useful when reporting errors.
* All properties are optional.
*/
public readonly properties: IExtractorMessageProperties;

/** @internal */
public constructor(options: IExtractorMessageOptions) {
this.category = options.category;
Expand All @@ -98,6 +123,7 @@ export class ExtractorMessage {
this.sourceFilePath = options.sourceFilePath;
this.sourceFileLine = options.sourceFileLine;
this.sourceFileColumn = options.sourceFileColumn;
this.properties = options.properties || { };
}

/**
Expand Down
10 changes: 8 additions & 2 deletions apps/api-extractor/src/api/ExtractorMessageId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ export const enum ExtractorMessageId {
/**
* The symbol ___ needs to be exported by the entry point ___.
*/
ForgottenExport = 'ae-forgotten-export'
ForgottenExport = 'ae-forgotten-export',

/**
* The name ___ should be prefixed with an underscore because the declaration is marked as `@internal`.
*/
InternalMissingUnderscore = 'ae-internal-missing-underscore'
}

export const allExtractorMessageIds: Set<string> = new Set<string>([
Expand All @@ -49,5 +54,6 @@ export const allExtractorMessageIds: Set<string> = new Set<string>([
'ae-incompatible-release-tags',
'ae-missing-release-tag',
'ae-misplaced-package-tag',
'ae-forgotten-export'
'ae-forgotten-export',
'ae-internal-missing-underscore'
]);
12 changes: 7 additions & 5 deletions apps/api-extractor/src/collector/MessageRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { AstSymbol } from '../analyzer/AstSymbol';
import {
ExtractorMessage,
ExtractorMessageCategory,
IExtractorMessageOptions
IExtractorMessageOptions,
IExtractorMessageProperties
} from '../api/ExtractorMessage';
import { ExtractorMessageId, allExtractorMessageIds } from '../api/ExtractorMessageId';
import {
Expand Down Expand Up @@ -164,7 +165,7 @@ export class MessageRouter {
* Add a message from the API Extractor analysis
*/
public addAnalyzerIssue(messageId: ExtractorMessageId, messageText: string,
astDeclarationOrSymbol: AstDeclaration | AstSymbol): void {
astDeclarationOrSymbol: AstDeclaration | AstSymbol, properties?: IExtractorMessageProperties): void {

let astDeclaration: AstDeclaration;
if (astDeclarationOrSymbol instanceof AstDeclaration) {
Expand All @@ -175,7 +176,7 @@ export class MessageRouter {

const extractorMessage: ExtractorMessage = this.addAnalyzerIssueForPosition(
messageId, messageText, astDeclaration.declaration.getSourceFile(),
astDeclaration.declaration.getStart());
astDeclaration.declaration.getStart(), properties);

this._associateMessageWithAstDeclaration(extractorMessage, astDeclaration);
}
Expand Down Expand Up @@ -231,7 +232,7 @@ export class MessageRouter {
* Add a message for a location in an arbitrary source file.
*/
public addAnalyzerIssueForPosition(messageId: ExtractorMessageId, messageText: string,
sourceFile: ts.SourceFile, pos: number): ExtractorMessage {
sourceFile: ts.SourceFile, pos: number, properties?: IExtractorMessageProperties): ExtractorMessage {

const lineAndCharacter: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(
pos);
Expand All @@ -242,7 +243,8 @@ export class MessageRouter {
text: messageText,
sourceFilePath: sourceFile.fileName,
sourceFileLine: lineAndCharacter.line + 1,
sourceFileColumn: lineAndCharacter.character + 1
sourceFileColumn: lineAndCharacter.character + 1,
properties
};

this._sourceMapper.updateExtractorMessageOptions(options);
Expand Down
22 changes: 22 additions & 0 deletions apps/api-extractor/src/collector/VisibilityChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,33 @@ export class VisibilityChecker {
VisibilityChecker._checkReferences(collector, astDeclaration, alreadyWarnedSymbols);
});

VisibilityChecker._checkForInternalUnderscore(collector, entity, entity.astEntity);
}
}
}
}

private static _checkForInternalUnderscore(collector: Collector, collectorEntity: CollectorEntity,
astSymbol: AstSymbol): void {

const astSymbolMetadata: SymbolMetadata = collector.fetchMetadata(astSymbol);

if (astSymbolMetadata.releaseTag === ReleaseTag.Internal && !astSymbolMetadata.releaseTagSameAsParent) {
for (const exportName of collectorEntity.exportNames) {
if (exportName[0] !== '_') {
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.InternalMissingUnderscore,
`The name ${exportName} should be prefixed with an underscore`
+ ` because the declaration is marked as "@internal"`,
astSymbol,
{ exportName }
);
}
}
}

}

private static _checkReferences(collector: Collector, astDeclaration: AstDeclaration,
alreadyWarnedSymbols: Set<AstSymbol>): void {

Expand Down
73 changes: 57 additions & 16 deletions apps/api-extractor/src/generators/ReviewFileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,46 @@ export class ReviewFileGenerator {
// Emit the regular declarations
for (const entity of collector.entities) {
if (entity.exported) {

// First, collect the list of export names for this symbol. When reporting messages with
// ExtractorMessage.properties.exportName, this will enable us to emit the warning comments alongside
// the associated export statement.
interface IExportToEmit {
readonly exportName: string;
readonly associatedMessages: ExtractorMessage[];
}
const exportsToEmit: Map<string, IExportToEmit> = new Map<string, IExportToEmit>();

for (const exportName of entity.exportNames) {
if (!entity.shouldInlineExport) {
exportsToEmit.set(exportName, { exportName, associatedMessages: [] });
}
}

if (entity.astEntity instanceof AstSymbol) {
// Emit all the declarations for this entry
// Emit all the declarations for this entity
for (const astDeclaration of entity.astEntity.astDeclarations || []) {

stringWriter.write(ReviewFileGenerator._getAedocSynopsis(collector, astDeclaration));
// Get the messages associated with this declaration
const fetchedMessages: ExtractorMessage[] = collector.messageRouter
.fetchAssociatedMessagesForReviewFile(astDeclaration);

// Peel off the messages associated with an export statement and store them
// in IExportToEmit.associatedMessages (to be processed later). The remaining messages will
// added to messagesToReport, to be emitted next to the declaration instead of the export statement.
const messagesToReport: ExtractorMessage[] = [];
for (const message of fetchedMessages) {
if (message.properties.exportName) {
const exportToEmit: IExportToEmit | undefined = exportsToEmit.get(message.properties.exportName);
if (exportToEmit) {
exportToEmit.associatedMessages.push(message);
continue;
}
}
messagesToReport.push(message);
}

stringWriter.write(ReviewFileGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport));

const span: Span = new Span(astDeclaration.declaration);
ReviewFileGenerator._modifySpan(collector, span, entity, astDeclaration, false);
Expand All @@ -75,11 +110,16 @@ export class ReviewFileGenerator {
}
}

for (const exportName of entity.exportNames) {
if (!entity.shouldInlineExport) {
DtsEmitHelpers.emitNamedExport(stringWriter, exportName, entity);
stringWriter.writeLine();
// Now emit the export statements for this entity.
for (const exportToEmit of exportsToEmit.values()) {
// Write any associated messages
for (const message of exportToEmit.associatedMessages) {
ReviewFileGenerator._writeLineAsComments(stringWriter,
'Warning: ' + message.formatMessageWithoutLocation());
}

DtsEmitHelpers.emitNamedExport(stringWriter, exportToEmit.exportName, entity);
stringWriter.writeLine();
}
}
}
Expand Down Expand Up @@ -245,7 +285,10 @@ export class ReviewFileGenerator {
}

if (!insideTypeLiteral) {
const aedocSynopsis: string = ReviewFileGenerator._getAedocSynopsis(collector, childAstDeclaration);
const messagesToReport: ExtractorMessage[] = collector.messageRouter
.fetchAssociatedMessagesForReviewFile(childAstDeclaration);
const aedocSynopsis: string = ReviewFileGenerator._getAedocSynopsis(collector, childAstDeclaration,
messagesToReport);
const indentedAedocSynopsis: string = ReviewFileGenerator._addIndentAfterNewlines(aedocSynopsis,
child.getIndent());

Expand All @@ -263,14 +306,12 @@ export class ReviewFileGenerator {
* whether the item has been documented, and any warnings that were detected
* by the analysis.
*/
private static _getAedocSynopsis(collector: Collector, astDeclaration: AstDeclaration): string {
const output: StringWriter = new StringWriter();

const messagesToReport: ExtractorMessage[] = collector.messageRouter
.fetchAssociatedMessagesForReviewFile(astDeclaration);
private static _getAedocSynopsis(collector: Collector, astDeclaration: AstDeclaration,
messagesToReport: ExtractorMessage[]): string {
const stringWriter: StringWriter = new StringWriter();

for (const message of messagesToReport) {
ReviewFileGenerator._writeLineAsComments(output, 'Warning: ' + message.formatMessageWithoutLocation());
ReviewFileGenerator._writeLineAsComments(stringWriter, 'Warning: ' + message.formatMessageWithoutLocation());
}

const declarationMetadata: DeclarationMetadata = collector.fetchMetadata(astDeclaration);
Expand Down Expand Up @@ -312,13 +353,13 @@ export class ReviewFileGenerator {

if (footerParts.length > 0) {
if (messagesToReport.length > 0) {
ReviewFileGenerator._writeLineAsComments(output, ''); // skip a line after the warnings
ReviewFileGenerator._writeLineAsComments(stringWriter, ''); // skip a line after the warnings
}

ReviewFileGenerator._writeLineAsComments(output, footerParts.join(' '));
ReviewFileGenerator._writeLineAsComments(stringWriter, footerParts.join(' '));
}

return output.toString();
return stringWriter.toString();
}

private static _writeLineAsComments(stringWriter: StringWriter, line: string): void {
Expand Down
6 changes: 5 additions & 1 deletion apps/api-extractor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
*/

export { Extractor, IAnalyzeProjectOptions, IExtractorOptions } from './api/Extractor';
export { ExtractorMessage, ExtractorMessageCategory } from './api/ExtractorMessage';
export {
ExtractorMessage,
IExtractorMessageProperties,
ExtractorMessageCategory
} from './api/ExtractorMessage';
export { ExtractorMessageId } from './api/ExtractorMessageId';
export {
IExtractorTsconfigCompilerConfig,
Expand Down
4 changes: 4 additions & 0 deletions apps/api-extractor/src/schemas/api-extractor-defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
"ae-incompatible-release-tags": {
"logLevel": "warning",
"addToApiReviewFile": true
},
"ae-internal-missing-underscore": {
"logLevel": "warning",
"addToApiReviewFile": true
}
},
"tsdocMessageReporting": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,6 @@
"canonicalReference": "",
"name": "",
"members": [
{
"kind": "Class",
"canonicalReference": "(A:class)",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "declare class "
},
{
"kind": "Reference",
"text": "A"
},
{
"kind": "Content",
"text": " "
}
],
"releaseTag": "Public",
"name": "A",
"members": [],
"implementsTokenRanges": []
},
{
"kind": "Class",
"canonicalReference": "(X:class)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
```ts

// @public (undocumented)
// @internal (undocumented)
class A {
}

// Warning: (ae-internal-missing-underscore) The name B should be prefixed with an underscore because the declaration is marked as "@internal"
export { A as B }

// Warning: (ae-internal-missing-underscore) The name C should be prefixed with an underscore because the declaration is marked as "@internal"
export { A as C }

// @public (undocumented)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

/** @public */
/** @internal */
declare class A {
}
export { A as B }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
export class X { }
export { X as Y }

/** @public */
/** @internal */
class A { }
// The underscore warning should get printed next to these export statements, not next to the class declaration
export { A as B }
export { A as C }
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,15 @@ export namespace EntangledNamespace {
// @alpha
export type ExportedAlias = AlphaClass;

// Warning: (ae-internal-missing-underscore) The name InternalClass should be prefixed with an underscore because the declaration is marked as "@internal"
//
// @internal
export class InternalClass {
undecoratedMember(): void;
}

// Warning: (ae-internal-missing-underscore) The name IPublicClassInternalParameters should be prefixed with an underscore because the declaration is marked as "@internal"
//
// @internal
export interface IPublicClassInternalParameters {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/api-extractor",
"comment": "Reintroduce \"ae-internal-missing-underscore\" warning for API items marked as `@internal` but whose name does not start with an underscore",
"type": "patch"
}
],
"packageName": "@microsoft/api-extractor",
"email": "[email protected]"
}
Loading

0 comments on commit 083a292

Please sign in to comment.