Reputation: 105
Hey there,
I'm trying to build something that would add comments on top of a function.
Unfortunately, it seems like ts.setSyntheticLeadingComments
does not allow me to replace existing comments.
I have tried:
ts.setSyntheticLeadingComments(node, [])
ts.setSyntheticLeadingComments(node, undefined)
node = ts.setSyntheticLeadingComments(node, [])
But none of these works. Ultimately, my goal was to be able to replace existing comments that I would have generated by the new ones.
Any idea ? Thanks 🙏
const transformFactory = (context: ts.TransformationContext) => (
rootNode: ts.SourceFile
): ts.SourceFile => {
const visit = (node: ts.Node) => {
node = ts.visitEachChild(node, visit, context);
ts.setSyntheticLeadingComments(node, []);
return node;
};
return ts.visitNode(rootNode, visit);
};
const sourceFile = ts.createSourceFile(
path,
source,
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.TS
);
const result = ts.transform(sourceFile, [transformFactory]);
const resultPrinter = ts.createPrinter({ removeComments: false });
console.log(resultPrinter.printFile(result.transformed[0]));
Try the following transformer, and see how no comments are removed at all
Using ts.createPrinter(..., { substituteNode(hint, node) { ... } })
doesn't help either
It seems like ts.getSyntheticLeadingComments()
doesn't work the way I expect it to either. It always returns undefined
, which lead me to use the following utils although I'm not sure to entirely understand the purpose of it (borrowed from https://github.com/angular/tsickle/blob/6f5835a644f3c628a61e3dcd558bb9c59c73dc2f/src/transformer_util.ts#L257-L266)
/**
* A replacement for ts.getLeadingCommentRanges that returns the union of synthetic and
* non-synthetic comments on the given node, with their text included. The returned comments must
* not be mutated, as their content might or might not be reflected back into the AST.
*/
export function getAllLeadingComments(node: ts.Node):
ReadonlyArray<Readonly<ts.CommentRange&{text: string}>> {
const allRanges: Array<Readonly<ts.CommentRange&{text: string}>> = [];
const nodeText = node.getFullText();
const cr = ts.getLeadingCommentRanges(nodeText, 0);
if (cr) allRanges.push(...cr.map(c => ({...c, text: nodeText.substring(c.pos, c.end)})));
const synthetic = ts.getSyntheticLeadingComments(node);
if (synthetic) allRanges.push(...synthetic);
return allRanges;
}
Upvotes: 2
Views: 697
Reputation: 249796
The problem is you expect the *SyntheticLeadingComments
functions to impact source comments. They will not. They will only impact comments that were previously synthetized (ie added by you in code).
Actual comments are not kept as nodes in the AST. You can get actual source comments using getLeadingCommentRanges
and getTrailingCommentRanges
.
A node has a start
and an end
position that do not include any comments. There is also a fullStart to the node which is the position including any leading comments. When a node is outputted this is how typescript knows how to copy the comments to the output.
If we use setTextRange
to set the range of a node to exclude these existing comments the result is we are effectively removing them from the output and we can add out new comments using setSyntheticLeadingComments
:
import * as ts from 'typescript'
const transformFactory = (context: ts.TransformationContext) => (
rootNode: ts.SourceFile
): ts.SourceFile => {
const visit = (node: ts.Node) => {
node = ts.visitEachChild(node, visit, context);
if(ts.isFunctionDeclaration(node)) {
let sourceFileText = node.getSourceFile().text;
const existingComments = ts.getLeadingCommentRanges(sourceFileText, node.pos);
if (existingComments) {
// Log existing comments just for fun
for (const comment of existingComments) {
console.log(sourceFileText.substring(comment.pos, comment.end))
}
// Comment also attaches to the first child, we must remove it recursively.
let removeComments = (c: ts.Node) => {
if (c.getFullStart() === node.getFullStart()) {
ts.setTextRange(c, { pos: c.getStart(), end: c.getEnd() });
}
c = ts.visitEachChild(c, removeComments, context);
return c;
}
ts.visitEachChild(node, removeComments, context);
ts.setTextRange(node, { pos: node.getStart(), end: node.getEnd() })
ts.setSyntheticLeadingComments(node, [{
pos: -1,
end: -1,
hasTrailingNewLine: false,
text: "Improved comment",
kind: ts.SyntaxKind.SingleLineCommentTrivia
}]);
}
}
return node;
};
return ts.visitNode(rootNode, visit);
};
const sourceFile = ts.createSourceFile(
"path.ts",
`
// Original comment
function test () {
}
`,
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.TS
);
const result = ts.transform(sourceFile, [transformFactory]);
const resultPrinter = ts.createPrinter({ removeComments: false });
console.log("!");
console.log(resultPrinter.printFile(result.transformed[0]));
Upvotes: 6