Skip to content

Commit 6194749

Browse files
Copiloticlanton
andcommitted
Add validation for multiple @module tags
- ModuleDocComment now accepts Collector parameter for error reporting - Detects and reports error for multiple @module tags in same file - Added MisplacedModuleTag error message ID - Maintains silent skip behavior for @module on individual declarations Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
1 parent 0ea3768 commit 6194749

4 files changed

Lines changed: 37 additions & 7 deletions

File tree

apps/api-extractor/src/aedoc/ModuleDocComment.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,50 @@
33

44
import * as ts from 'typescript';
55

6+
import type { Collector } from '../collector/Collector';
7+
import { ExtractorMessageId } from '../api/ExtractorMessageId';
8+
69
export class ModuleDocComment {
710
/**
811
* For the given source file, see if it starts with a TSDoc comment containing the `@module` tag.
912
*/
10-
public static tryFindInSourceFile(sourceFile: ts.SourceFile): ts.TextRange | undefined {
13+
public static tryFindInSourceFile(
14+
sourceFile: ts.SourceFile,
15+
collector: Collector
16+
): ts.TextRange | undefined {
1117
// The @module comment is special because it is not attached to an AST
1218
// definition. Instead, it is part of the "trivia" tokens that the compiler treats
1319
// as irrelevant white space.
1420
//
1521
// This implementation assumes that the "@module" will be in the first TSDoc comment
1622
// that appears in the source file.
1723
let moduleCommentRange: ts.TextRange | undefined = undefined;
24+
let foundFirstJSDocComment: boolean = false;
1825

1926
for (const commentRange of ts.getLeadingCommentRanges(sourceFile.text, sourceFile.getFullStart()) || []) {
2027
if (commentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia) {
2128
const commentBody: string = sourceFile.text.substring(commentRange.pos, commentRange.end);
2229

2330
// Choose the first JSDoc-style comment
2431
if (/^\s*\/\*\*/.test(commentBody)) {
25-
// But only if it looks like it's trying to be @module
26-
// (The TSDoc parser will validate this more rigorously)
27-
if (/\@module/i.test(commentBody)) {
28-
moduleCommentRange = commentRange;
32+
if (!foundFirstJSDocComment) {
33+
foundFirstJSDocComment = true;
34+
// But only if it looks like it's trying to be @module
35+
// (The TSDoc parser will validate this more rigorously)
36+
if (/\@module/i.test(commentBody)) {
37+
moduleCommentRange = commentRange;
38+
}
39+
} else {
40+
// If we find another JSDoc comment with @module, report an error
41+
if (/\@module/i.test(commentBody)) {
42+
collector.messageRouter.addAnalyzerIssueForPosition(
43+
ExtractorMessageId.MisplacedModuleTag,
44+
'The @module comment should only appear once at the top of the source file',
45+
sourceFile,
46+
commentRange.pos
47+
);
48+
}
2949
}
30-
break;
3150
}
3251
}
3352
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export enum ExtractorMessageId {
5656
*/
5757
MisplacedPackageTag = 'ae-misplaced-package-tag',
5858

59+
/**
60+
* "The `@module` comment must only appear on modules that are re-exported as namespaces."
61+
*/
62+
MisplacedModuleTag = 'ae-misplaced-module-tag',
63+
5964
/**
6065
* "The symbol ___ needs to be exported by the entry point ___."
6166
*/
@@ -130,6 +135,7 @@ export const allExtractorMessageIds: Set<string> = new Set<string>([
130135
'ae-incompatible-release-tags',
131136
'ae-missing-release-tag',
132137
'ae-misplaced-package-tag',
138+
'ae-misplaced-module-tag',
133139
'ae-forgotten-export',
134140
'ae-internal-missing-underscore',
135141
'ae-internal-mixed-release-tag',

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { IAstModuleExportInfo } from '../analyzer/AstModule';
2323
import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter';
2424
import type { AstEntity } from '../analyzer/AstEntity';
2525
import { ModuleDocComment } from '../aedoc/ModuleDocComment';
26+
import { ExtractorMessageId } from '../api/ExtractorMessageId';
2627

2728
/**
2829
* Used with DtsRollupGenerator.writeTypingsFile()
@@ -184,7 +185,10 @@ export class DtsRollupGenerator {
184185

185186
// Check if the source file has a @module comment and emit it before the namespace declaration
186187
const sourceFile: ts.SourceFile = astEntity.astModule.sourceFile;
187-
const moduleCommentRange: ts.TextRange | undefined = ModuleDocComment.tryFindInSourceFile(sourceFile);
188+
const moduleCommentRange: ts.TextRange | undefined = ModuleDocComment.tryFindInSourceFile(
189+
sourceFile,
190+
collector
191+
);
188192
if (moduleCommentRange) {
189193
const moduleComment: string = sourceFile.text.substring(
190194
moduleCommentRange.pos,

common/reviews/api/api-extractor.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export enum ExtractorMessageId {
151151
IncompatibleReleaseTags = "ae-incompatible-release-tags",
152152
InternalMissingUnderscore = "ae-internal-missing-underscore",
153153
InternalMixedReleaseTag = "ae-internal-mixed-release-tag",
154+
MisplacedModuleTag = "ae-misplaced-module-tag",
154155
MisplacedPackageTag = "ae-misplaced-package-tag",
155156
MissingGetter = "ae-missing-getter",
156157
MissingReleaseTag = "ae-missing-release-tag",

0 commit comments

Comments
 (0)