mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-16 05:58:32 -06:00
* Add test with bug * Fix for import placement * Consolidate comment recognition functions into utilities * Add another test with all 3 kinds * Recognize path directives as part of triple slash directives * Also handle no-default-lib triple-slash comments * Test for all the triple-slash kinds * Keep import-placement logic in the quickfix, since its not really a node start; accept new baselines * Work in not-ES6, use a real no-lib comment * Remove no default lib triple slash comment, it disables checking and thereby quick fixes * Copy regex rather than have a regex copy
421 lines
18 KiB
TypeScript
421 lines
18 KiB
TypeScript
/// <reference path="sourcemap.ts" />
|
|
|
|
/* @internal */
|
|
namespace ts {
|
|
export interface CommentWriter {
|
|
reset(): void;
|
|
setSourceFile(sourceFile: SourceFile): void;
|
|
setWriter(writer: EmitTextWriter): void;
|
|
emitNodeWithComments(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void;
|
|
emitBodyWithDetachedComments(node: Node, detachedRange: TextRange, emitCallback: (node: Node) => void): void;
|
|
emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean): void;
|
|
emitLeadingCommentsOfPosition(pos: number): void;
|
|
}
|
|
|
|
export function createCommentWriter(printerOptions: PrinterOptions, emitPos: ((pos: number) => void) | undefined): CommentWriter {
|
|
const extendedDiagnostics = printerOptions.extendedDiagnostics;
|
|
const newLine = getNewLineCharacter(printerOptions);
|
|
let writer: EmitTextWriter;
|
|
let containerPos = -1;
|
|
let containerEnd = -1;
|
|
let declarationListContainerEnd = -1;
|
|
let currentSourceFile: SourceFile;
|
|
let currentText: string;
|
|
let currentLineMap: number[];
|
|
let detachedCommentsInfo: { nodePos: number, detachedCommentEndPos: number}[];
|
|
let hasWrittenComment = false;
|
|
let disabled: boolean = printerOptions.removeComments;
|
|
|
|
return {
|
|
reset,
|
|
setWriter,
|
|
setSourceFile,
|
|
emitNodeWithComments,
|
|
emitBodyWithDetachedComments,
|
|
emitTrailingCommentsOfPosition,
|
|
emitLeadingCommentsOfPosition,
|
|
};
|
|
|
|
function emitNodeWithComments(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) {
|
|
if (disabled) {
|
|
emitCallback(hint, node);
|
|
return;
|
|
}
|
|
|
|
if (node) {
|
|
hasWrittenComment = false;
|
|
|
|
const emitNode = node.emitNode;
|
|
const emitFlags = emitNode && emitNode.flags;
|
|
const { pos, end } = emitNode && emitNode.commentRange || node;
|
|
if ((pos < 0 && end < 0) || (pos === end)) {
|
|
// Both pos and end are synthesized, so just emit the node without comments.
|
|
emitNodeWithSynthesizedComments(hint, node, emitNode, emitFlags, emitCallback);
|
|
}
|
|
else {
|
|
if (extendedDiagnostics) {
|
|
performance.mark("preEmitNodeWithComment");
|
|
}
|
|
|
|
const isEmittedNode = node.kind !== SyntaxKind.NotEmittedStatement;
|
|
// We have to explicitly check that the node is JsxText because if the compilerOptions.jsx is "preserve" we will not do any transformation.
|
|
// It is expensive to walk entire tree just to set one kind of node to have no comments.
|
|
const skipLeadingComments = pos < 0 || (emitFlags & EmitFlags.NoLeadingComments) !== 0 || node.kind === SyntaxKind.JsxText;
|
|
const skipTrailingComments = end < 0 || (emitFlags & EmitFlags.NoTrailingComments) !== 0 || node.kind === SyntaxKind.JsxText;
|
|
|
|
// Emit leading comments if the position is not synthesized and the node
|
|
// has not opted out from emitting leading comments.
|
|
if (!skipLeadingComments) {
|
|
emitLeadingComments(pos, isEmittedNode);
|
|
}
|
|
|
|
// Save current container state on the stack.
|
|
const savedContainerPos = containerPos;
|
|
const savedContainerEnd = containerEnd;
|
|
const savedDeclarationListContainerEnd = declarationListContainerEnd;
|
|
|
|
if (!skipLeadingComments) {
|
|
containerPos = pos;
|
|
}
|
|
|
|
if (!skipTrailingComments) {
|
|
containerEnd = end;
|
|
|
|
// To avoid invalid comment emit in a down-level binding pattern, we
|
|
// keep track of the last declaration list container's end
|
|
if (node.kind === SyntaxKind.VariableDeclarationList) {
|
|
declarationListContainerEnd = end;
|
|
}
|
|
}
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.measure("commentTime", "preEmitNodeWithComment");
|
|
}
|
|
|
|
emitNodeWithSynthesizedComments(hint, node, emitNode, emitFlags, emitCallback);
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.mark("postEmitNodeWithComment");
|
|
}
|
|
|
|
// Restore previous container state.
|
|
containerPos = savedContainerPos;
|
|
containerEnd = savedContainerEnd;
|
|
declarationListContainerEnd = savedDeclarationListContainerEnd;
|
|
|
|
// Emit trailing comments if the position is not synthesized and the node
|
|
// has not opted out from emitting leading comments and is an emitted node.
|
|
if (!skipTrailingComments && isEmittedNode) {
|
|
emitTrailingComments(end);
|
|
}
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.measure("commentTime", "postEmitNodeWithComment");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitNodeWithSynthesizedComments(hint: EmitHint, node: Node, emitNode: EmitNode, emitFlags: EmitFlags, emitCallback: (hint: EmitHint, node: Node) => void) {
|
|
const leadingComments = emitNode && emitNode.leadingComments;
|
|
if (some(leadingComments)) {
|
|
if (extendedDiagnostics) {
|
|
performance.mark("preEmitNodeWithSynthesizedComments");
|
|
}
|
|
|
|
forEach(leadingComments, emitLeadingSynthesizedComment);
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.measure("commentTime", "preEmitNodeWithSynthesizedComments");
|
|
}
|
|
}
|
|
|
|
emitNodeWithNestedComments(hint, node, emitFlags, emitCallback);
|
|
|
|
const trailingComments = emitNode && emitNode.trailingComments;
|
|
if (some(trailingComments)) {
|
|
if (extendedDiagnostics) {
|
|
performance.mark("postEmitNodeWithSynthesizedComments");
|
|
}
|
|
|
|
forEach(trailingComments, emitTrailingSynthesizedComment);
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.measure("commentTime", "postEmitNodeWithSynthesizedComments");
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitLeadingSynthesizedComment(comment: SynthesizedComment) {
|
|
if (comment.kind === SyntaxKind.SingleLineCommentTrivia) {
|
|
writer.writeLine();
|
|
}
|
|
writeSynthesizedComment(comment);
|
|
if (comment.hasTrailingNewLine || comment.kind === SyntaxKind.SingleLineCommentTrivia) {
|
|
writer.writeLine();
|
|
}
|
|
else {
|
|
writer.write(" ");
|
|
}
|
|
}
|
|
|
|
function emitTrailingSynthesizedComment(comment: SynthesizedComment) {
|
|
if (!writer.isAtStartOfLine()) {
|
|
writer.write(" ");
|
|
}
|
|
writeSynthesizedComment(comment);
|
|
if (comment.hasTrailingNewLine) {
|
|
writer.writeLine();
|
|
}
|
|
}
|
|
|
|
function writeSynthesizedComment(comment: SynthesizedComment) {
|
|
const text = formatSynthesizedComment(comment);
|
|
const lineMap = comment.kind === SyntaxKind.MultiLineCommentTrivia ? computeLineStarts(text) : undefined;
|
|
writeCommentRange(text, lineMap, writer, 0, text.length, newLine);
|
|
}
|
|
|
|
function formatSynthesizedComment(comment: SynthesizedComment) {
|
|
return comment.kind === SyntaxKind.MultiLineCommentTrivia
|
|
? `/*${comment.text}*/`
|
|
: `//${comment.text}`;
|
|
}
|
|
|
|
function emitNodeWithNestedComments(hint: EmitHint, node: Node, emitFlags: EmitFlags, emitCallback: (hint: EmitHint, node: Node) => void) {
|
|
if (emitFlags & EmitFlags.NoNestedComments) {
|
|
disabled = true;
|
|
emitCallback(hint, node);
|
|
disabled = false;
|
|
}
|
|
else {
|
|
emitCallback(hint, node);
|
|
}
|
|
}
|
|
|
|
function emitBodyWithDetachedComments(node: Node, detachedRange: TextRange, emitCallback: (node: Node) => void) {
|
|
if (extendedDiagnostics) {
|
|
performance.mark("preEmitBodyWithDetachedComments");
|
|
}
|
|
|
|
const { pos, end } = detachedRange;
|
|
const emitFlags = getEmitFlags(node);
|
|
const skipLeadingComments = pos < 0 || (emitFlags & EmitFlags.NoLeadingComments) !== 0;
|
|
const skipTrailingComments = disabled || end < 0 || (emitFlags & EmitFlags.NoTrailingComments) !== 0;
|
|
|
|
if (!skipLeadingComments) {
|
|
emitDetachedCommentsAndUpdateCommentsInfo(detachedRange);
|
|
}
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.measure("commentTime", "preEmitBodyWithDetachedComments");
|
|
}
|
|
|
|
if (emitFlags & EmitFlags.NoNestedComments && !disabled) {
|
|
disabled = true;
|
|
emitCallback(node);
|
|
disabled = false;
|
|
}
|
|
else {
|
|
emitCallback(node);
|
|
}
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.mark("beginEmitBodyWithDetachedCommetns");
|
|
}
|
|
|
|
if (!skipTrailingComments) {
|
|
emitLeadingComments(detachedRange.end, /*isEmittedNode*/ true);
|
|
if (hasWrittenComment && !writer.isAtStartOfLine()) {
|
|
writer.writeLine();
|
|
}
|
|
}
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.measure("commentTime", "beginEmitBodyWithDetachedCommetns");
|
|
}
|
|
}
|
|
|
|
function emitLeadingComments(pos: number, isEmittedNode: boolean) {
|
|
hasWrittenComment = false;
|
|
|
|
if (isEmittedNode) {
|
|
forEachLeadingCommentToEmit(pos, emitLeadingComment);
|
|
}
|
|
else if (pos === 0) {
|
|
// If the node will not be emitted in JS, remove all the comments(normal, pinned and ///) associated with the node,
|
|
// unless it is a triple slash comment at the top of the file.
|
|
// For Example:
|
|
// /// <reference-path ...>
|
|
// declare var x;
|
|
// /// <reference-path ...>
|
|
// interface F {}
|
|
// The first /// will NOT be removed while the second one will be removed even though both node will not be emitted
|
|
forEachLeadingCommentToEmit(pos, emitTripleSlashLeadingComment);
|
|
}
|
|
}
|
|
|
|
function emitTripleSlashLeadingComment(commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) {
|
|
if (isTripleSlashComment(commentPos, commentEnd)) {
|
|
emitLeadingComment(commentPos, commentEnd, kind, hasTrailingNewLine, rangePos);
|
|
}
|
|
}
|
|
|
|
function emitLeadingComment(commentPos: number, commentEnd: number, _kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) {
|
|
if (!hasWrittenComment) {
|
|
emitNewLineBeforeLeadingCommentOfPosition(currentLineMap, writer, rangePos, commentPos);
|
|
hasWrittenComment = true;
|
|
}
|
|
|
|
// Leading comments are emitted at /*leading comment1 */space/*leading comment*/space
|
|
if (emitPos) emitPos(commentPos);
|
|
writeCommentRange(currentText, currentLineMap, writer, commentPos, commentEnd, newLine);
|
|
if (emitPos) emitPos(commentEnd);
|
|
|
|
if (hasTrailingNewLine) {
|
|
writer.writeLine();
|
|
}
|
|
else {
|
|
writer.write(" ");
|
|
}
|
|
}
|
|
|
|
function emitLeadingCommentsOfPosition(pos: number) {
|
|
if (disabled || pos === -1) {
|
|
return;
|
|
}
|
|
|
|
emitLeadingComments(pos, /*isEmittedNode*/ true);
|
|
}
|
|
|
|
function emitTrailingComments(pos: number) {
|
|
forEachTrailingCommentToEmit(pos, emitTrailingComment);
|
|
}
|
|
|
|
function emitTrailingComment(commentPos: number, commentEnd: number, _kind: SyntaxKind, hasTrailingNewLine: boolean) {
|
|
// trailing comments are emitted at space/*trailing comment1 */space/*trailing comment2*/
|
|
if (!writer.isAtStartOfLine()) {
|
|
writer.write(" ");
|
|
}
|
|
|
|
if (emitPos) emitPos(commentPos);
|
|
writeCommentRange(currentText, currentLineMap, writer, commentPos, commentEnd, newLine);
|
|
if (emitPos) emitPos(commentEnd);
|
|
|
|
if (hasTrailingNewLine) {
|
|
writer.writeLine();
|
|
}
|
|
}
|
|
|
|
function emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean) {
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.mark("beforeEmitTrailingCommentsOfPosition");
|
|
}
|
|
|
|
forEachTrailingCommentToEmit(pos, prefixSpace ? emitTrailingComment : emitTrailingCommentOfPosition);
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.measure("commentTime", "beforeEmitTrailingCommentsOfPosition");
|
|
}
|
|
}
|
|
|
|
function emitTrailingCommentOfPosition(commentPos: number, commentEnd: number, _kind: SyntaxKind, hasTrailingNewLine: boolean) {
|
|
// trailing comments of a position are emitted at /*trailing comment1 */space/*trailing comment*/space
|
|
|
|
if (emitPos) emitPos(commentPos);
|
|
writeCommentRange(currentText, currentLineMap, writer, commentPos, commentEnd, newLine);
|
|
if (emitPos) emitPos(commentEnd);
|
|
|
|
if (hasTrailingNewLine) {
|
|
writer.writeLine();
|
|
}
|
|
else {
|
|
writer.write(" ");
|
|
}
|
|
}
|
|
|
|
function forEachLeadingCommentToEmit(pos: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) => void) {
|
|
// Emit the leading comments only if the container's pos doesn't match because the container should take care of emitting these comments
|
|
if (containerPos === -1 || pos !== containerPos) {
|
|
if (hasDetachedComments(pos)) {
|
|
forEachLeadingCommentWithoutDetachedComments(cb);
|
|
}
|
|
else {
|
|
forEachLeadingCommentRange(currentText, pos, cb, /*state*/ pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
function forEachTrailingCommentToEmit(end: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => void) {
|
|
// Emit the trailing comments only if the container's end doesn't match because the container should take care of emitting these comments
|
|
if (containerEnd === -1 || (end !== containerEnd && end !== declarationListContainerEnd)) {
|
|
forEachTrailingCommentRange(currentText, end, cb);
|
|
}
|
|
}
|
|
|
|
function reset() {
|
|
currentSourceFile = undefined;
|
|
currentText = undefined;
|
|
currentLineMap = undefined;
|
|
detachedCommentsInfo = undefined;
|
|
}
|
|
|
|
function setWriter(output: EmitTextWriter): void {
|
|
writer = output;
|
|
}
|
|
|
|
function setSourceFile(sourceFile: SourceFile) {
|
|
currentSourceFile = sourceFile;
|
|
currentText = currentSourceFile.text;
|
|
currentLineMap = getLineStarts(currentSourceFile);
|
|
detachedCommentsInfo = undefined;
|
|
}
|
|
|
|
function hasDetachedComments(pos: number) {
|
|
return detachedCommentsInfo !== undefined && lastOrUndefined(detachedCommentsInfo).nodePos === pos;
|
|
}
|
|
|
|
function forEachLeadingCommentWithoutDetachedComments(cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) => void) {
|
|
// get the leading comments from detachedPos
|
|
const pos = lastOrUndefined(detachedCommentsInfo).detachedCommentEndPos;
|
|
if (detachedCommentsInfo.length - 1) {
|
|
detachedCommentsInfo.pop();
|
|
}
|
|
else {
|
|
detachedCommentsInfo = undefined;
|
|
}
|
|
|
|
forEachLeadingCommentRange(currentText, pos, cb, /*state*/ pos);
|
|
}
|
|
|
|
function emitDetachedCommentsAndUpdateCommentsInfo(range: TextRange) {
|
|
const currentDetachedCommentInfo = emitDetachedComments(currentText, currentLineMap, writer, writeComment, range, newLine, disabled);
|
|
if (currentDetachedCommentInfo) {
|
|
if (detachedCommentsInfo) {
|
|
detachedCommentsInfo.push(currentDetachedCommentInfo);
|
|
}
|
|
else {
|
|
detachedCommentsInfo = [currentDetachedCommentInfo];
|
|
}
|
|
}
|
|
}
|
|
|
|
function writeComment(text: string, lineMap: number[], writer: EmitTextWriter, commentPos: number, commentEnd: number, newLine: string) {
|
|
if (emitPos) emitPos(commentPos);
|
|
writeCommentRange(text, lineMap, writer, commentPos, commentEnd, newLine);
|
|
if (emitPos) emitPos(commentEnd);
|
|
}
|
|
|
|
/**
|
|
* Determine if the given comment is a triple-slash
|
|
*
|
|
* @return true if the comment is a triple-slash comment else false
|
|
*/
|
|
function isTripleSlashComment(commentPos: number, commentEnd: number) {
|
|
return isRecognizedTripleSlashComment(currentText, commentPos, commentEnd);
|
|
}
|
|
}
|
|
} |