in src/fileoverview_comment_transformer.ts [98:207]
export function transformFileoverviewCommentFactory(
options: ts.CompilerOptions, diagnostics: ts.Diagnostic[]) {
return (): (sourceFile: ts.SourceFile) => ts.SourceFile => {
function checkNoFileoverviewComments(
context: ts.Node, comments: jsdoc.SynthesizedCommentWithOriginal[], message: string) {
for (const comment of comments) {
const parse = jsdoc.parse(comment);
if (parse !== null && parse.tags.some(t => FILEOVERVIEW_COMMENT_MARKERS.has(t.tagName))) {
// Report a warning; this should not break compilation in third party code.
reportDiagnostic(
diagnostics, context, message, comment.originalRange, ts.DiagnosticCategory.Warning);
}
}
}
return (sourceFile: ts.SourceFile) => {
// TypeScript supports including some other file formats in compilation
// (JS, JSON). Avoid adding comments to those.
if (!sourceFile.fileName.match(/\.tsx?$/)) {
return sourceFile;
}
const text = sourceFile.getFullText();
let fileComments: ts.SynthesizedComment[] = [];
const firstStatement = sourceFile.statements.length && sourceFile.statements[0] || null;
const originalComments = ts.getLeadingCommentRanges(text, 0) || [];
if (!firstStatement) {
// In an empty source file, all comments are file-level comments.
fileComments = synthesizeCommentRanges(sourceFile, originalComments);
} else {
// Search for the last comment split from the file with a \n\n. All comments before that are
// considered fileoverview comments, all comments after that belong to the next
// statement(s). If none found, comments remains empty, and the code below will insert a new
// fileoverview comment.
for (let i = originalComments.length - 1; i >= 0; i--) {
const end = originalComments[i].end;
if (!text.substring(end).startsWith('\n\n') &&
!text.substring(end).startsWith('\r\n\r\n')) {
continue;
}
// This comment is separated from the source file with a double break, marking it (and any
// preceding comments) as a file-level comment. Split them off and attach them onto a
// NotEmittedStatement, so that they do not get lost later on.
const synthesizedComments = jsdoc.synthesizeLeadingComments(firstStatement);
const notEmitted = ts.createNotEmittedStatement(sourceFile);
// Modify the comments on the firstStatement in place by removing the file-level comments.
fileComments = synthesizedComments.splice(0, i + 1);
// Move the fileComments onto notEmitted.
ts.setSyntheticLeadingComments(notEmitted, fileComments);
sourceFile = updateSourceFileNode(
sourceFile,
ts.createNodeArray([notEmitted, firstStatement, ...sourceFile.statements.slice(1)]));
break;
}
// Now walk every top level statement and escape/drop any @fileoverview comments found.
// Closure ignores all @fileoverview comments but the last, so tsickle must make sure not to
// emit duplicated ones.
for (let i = 0; i < sourceFile.statements.length; i++) {
const stmt = sourceFile.statements[i];
// Accept the NotEmittedStatement inserted above.
if (i === 0 && stmt.kind === ts.SyntaxKind.NotEmittedStatement) {
continue;
}
const comments = jsdoc.synthesizeLeadingComments(stmt);
checkNoFileoverviewComments(
stmt, comments,
`file comments must be at the top of the file, ` +
`separated from the file body by an empty line.`);
}
}
// Closure Compiler considers the *last* comment with @fileoverview (or #externs or
// @nocompile) that has not been attached to some other tree node to be the file overview
// comment, and only applies @suppress tags from it. Google-internal tooling considers *any*
// comment mentioning @fileoverview.
let fileoverviewIdx = -1;
let tags: jsdoc.Tag[] = [];
for (let i = fileComments.length - 1; i >= 0; i--) {
const parse = jsdoc.parseContents(fileComments[i].text);
if (parse !== null && parse.tags.some(t => FILEOVERVIEW_COMMENT_MARKERS.has(t.tagName))) {
fileoverviewIdx = i;
tags = parse.tags;
break;
}
}
if (fileoverviewIdx !== -1) {
checkNoFileoverviewComments(
firstStatement || sourceFile, fileComments.slice(0, fileoverviewIdx),
`duplicate file level comment`);
}
augmentFileoverviewComments(options, sourceFile, tags);
const commentText = jsdoc.toStringWithoutStartEnd(tags);
if (fileoverviewIdx < 0) {
// No existing comment to merge with, just emit a new one.
return addNewFileoverviewComment(sourceFile, commentText);
}
fileComments[fileoverviewIdx].text = commentText;
// sf does not need to be updated, synthesized comments are mutable.
return sourceFile;
};
};
}