tsickle
Version:
Transpile TypeScript code to JavaScript with Closure annotations.
211 lines • 11.1 kB
JavaScript
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.transformFileoverviewCommentFactory = void 0;
const ts = require("typescript");
const jsdoc = require("./jsdoc");
const path = require("./path");
const transformer_util_1 = require("./transformer_util");
/**
* A set of JSDoc tags that mark a comment as a fileoverview comment. These are
* recognized by other pieces of infrastructure (Closure Compiler, module
* system, ...).
*/
const FILEOVERVIEW_COMMENT_MARKERS = new Set(['fileoverview', 'externs', 'modName', 'mods', 'pintomodule']);
/**
* Given a parsed \@fileoverview comment, ensures it has all the attributes we
* need. This function can be called to modify an existing comment or to make a
* new one.
*
* @param source Original TS source file. Its path is added in \@fileoverview.
* @param tags Comment as parsed list of tags; modified in-place.
*/
function augmentFileoverviewComments(options, source, tags, generateExtraSuppressions) {
// Ensure we start with a @fileoverview.
let fileOverview = tags.find(t => t.tagName === 'fileoverview');
if (!fileOverview) {
fileOverview = { tagName: 'fileoverview', text: 'added by tsickle' };
tags.splice(0, 0, fileOverview);
}
if (options.rootDir != null) {
const GENERATED_FROM_COMMENT_TEXT = `\n${jsdoc.createGeneratedFromComment(path.relative(options.rootDir, source.fileName))}`;
fileOverview.text = fileOverview.text ?
fileOverview.text + GENERATED_FROM_COMMENT_TEXT :
GENERATED_FROM_COMMENT_TEXT;
}
// Find or create a @suppress tag.
// TODO(b/235529020): Allow generating @fileoverview blocks with multiple
// different @suppress tags to make tsickle output more readable.
let suppressTag = tags.find(t => t.tagName === 'suppress');
let suppressions;
if (suppressTag) {
suppressions =
new Set((suppressTag.type || '').split(',').map(s => s.trim()));
}
else if (generateExtraSuppressions) {
suppressTag = { tagName: 'suppress', text: '' };
// Special case the @license tag because all text following this tag is
// treated by the compiler as part of the license, so we need to place the
// new @suppress tag before @license.
const licenseTagIndex = tags.findIndex(t => t.tagName === 'license');
if (licenseTagIndex !== -1) {
tags.splice(licenseTagIndex, 0, suppressTag);
}
else {
tags.push(suppressTag);
}
suppressions = new Set();
}
if (generateExtraSuppressions) {
// Ensure our suppressions are included in the @suppress tag:
// * Suppress checkTypes. We believe the code has already been type-checked
// by TypeScript, and we cannot model all the TypeScript type decisions in
// Closure syntax.
suppressions.add('checkTypes');
// * Suppress extraRequire. We remove extra requires at the TypeScript
// level, so any require that gets to the JS level is a load-bearing
// require.
suppressions.add('extraRequire');
// * Types references are propagated between files even when they are not
// directly imported. While these are violations of the "missing require"
// rules they are believed to be safe.
suppressions.add('missingRequire');
// * Suppress uselessCode. We emit an "if (false)" around type
// declarations, which is flagged as unused code unless we suppress it.
suppressions.add('uselessCode');
// * Suppress some checks for user errors that TS already checks.
suppressions.add('missingReturn');
suppressions.add('unusedPrivateMembers');
// * Suppress checking for @override, because TS doesn't model it.
suppressions.add('missingOverride');
// * Suppress const JSCompiler errors in TS file.
// a) TypeScript already checks for "const" and
// b) there are various JSCompiler false positives
suppressions.add('const');
}
if (suppressTag) {
suppressTag.type = Array.from(suppressions.values()).sort().join(',');
}
return tags;
}
/**
* A transformer that ensures the emitted JS file has an \@fileoverview comment
* that contains an
* \@suppress {checkTypes} annotation by either adding or updating an existing
* comment.
*/
function transformFileoverviewCommentFactory(options, diagnostics, generateExtraSuppressions) {
return () => {
function checkNoFileoverviewComments(context, comments, message) {
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.
(0, transformer_util_1.reportDiagnostic)(diagnostics, context, message, comment.originalRange, ts.DiagnosticCategory.Warning);
}
}
}
return (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 = [];
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 = (0, transformer_util_1.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.factory.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 =
(0, transformer_util_1.updateSourceFileNode)(sourceFile, ts.factory.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 = [];
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, generateExtraSuppressions);
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;
};
};
}
exports.transformFileoverviewCommentFactory = transformFileoverviewCommentFactory;
function addNewFileoverviewComment(sf, commentText) {
let syntheticFirstStatement = (0, transformer_util_1.createNotEmittedStatement)(sf);
syntheticFirstStatement = ts.addSyntheticTrailingComment(syntheticFirstStatement, ts.SyntaxKind.MultiLineCommentTrivia, commentText, true);
return (0, transformer_util_1.updateSourceFileNode)(sf, ts.factory.createNodeArray([syntheticFirstStatement, ...sf.statements]));
}
//# sourceMappingURL=fileoverview_comment_transformer.js.map
;