///
/* @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): 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;
const skipLeadingComments = pos < 0 || (emitFlags & EmitFlags.NoLeadingComments) !== 0;
const skipTrailingComments = end < 0 || (emitFlags & EmitFlags.NoTrailingComments) !== 0;
// 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:
// ///
// declare var x;
// ///
// 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) {
if (disabled) {
return;
}
if (extendedDiagnostics) {
performance.mark("beforeEmitTrailingCommentsOfPosition");
}
forEachTrailingCommentToEmit(pos, 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) {
// Verify this is /// comment, but do the regexp match only when we first can find /// in the comment text
// so that we don't end up computing comment string and doing match for all // comments
if (currentText.charCodeAt(commentPos + 1) === CharacterCodes.slash &&
commentPos + 2 < commentEnd &&
currentText.charCodeAt(commentPos + 2) === CharacterCodes.slash) {
const textSubStr = currentText.substring(commentPos, commentEnd);
return textSubStr.match(fullTripleSlashReferencePathRegEx) ||
textSubStr.match(fullTripleSlashAMDReferencePathRegEx) ?
true : false;
}
return false;
}
}
}