Skip to content

Commit da2f84d

Browse files
Rich-Harrismanuel3108jycouet
authored
feat: getLeadingComments/getTrailingComments (#90)
* feat: add additionalComments option for programmatic comment insertion * fix casing * add type export * fix multi-line comments * update `load` to `acornParse` * feat: `getLeadingComments`/`getTrailingComments` * unused * expose BaseComment * oops * Update .changeset/pretty-spiders-hear.md Co-authored-by: Manuel <30698007+manuel3108@users.noreply.github.com> * update README --------- Co-authored-by: Manuel Serret <mserret99@gmail.com> Co-authored-by: jycouet <jycouet@gmail.com> Co-authored-by: Manuel <30698007+manuel3108@users.noreply.github.com>
1 parent 065dd05 commit da2f84d

File tree

7 files changed

+180
-6
lines changed

7 files changed

+180
-6
lines changed

.changeset/pretty-spiders-hear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'esrap': minor
3+
---
4+
5+
feat: add `getLeadingComments` & `getTrailingComments` option for programmatic comment insertion

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ const { code, map } = print(
5555
quotes: 'single',
5656

5757
// an array of `{ type: 'Line' | 'Block', value: string, loc: { start, end } }` objects
58-
comments: []
58+
comments: [],
59+
60+
// a pair of functions for inserting additional comments before or after a given node.
61+
// returns `Array<{ type: 'Line' | 'Block', value: string }>` or `undefined`
62+
getLeadingComments: (node) => [{ type: 'Line', value: ' a comment before the node' }],
63+
getTrailingComments: (node) => [{ type: 'Block', value: ' a comment after the node' }]
5964
})
6065
);
6166
```

src/languages/ts/index.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { TSESTree } from '@typescript-eslint/types' */
22
/** @import { Visitors } from '../../types.js' */
3-
/** @import { TSOptions, Comment } from '../types.js' */
3+
/** @import { TSOptions, BaseComment } from '../types.js' */
44
import { Context } from 'esrap';
55

66
/** @typedef {TSESTree.Node} Node */
@@ -74,7 +74,7 @@ const OPERATOR_PRECEDENCE = {
7474
};
7575

7676
/**
77-
* @param {Comment} comment
77+
* @param {BaseComment} comment
7878
* @param {Context} context
7979
*/
8080
function write_comment(comment, context) {
@@ -90,6 +90,7 @@ function write_comment(comment, context) {
9090
}
9191

9292
context.write('*/');
93+
if (lines.length > 1) context.newline();
9394
}
9495
}
9596

@@ -104,6 +105,36 @@ export default (options = {}) => {
104105

105106
let comment_index = 0;
106107

108+
/**
109+
* Write additional comments for a node
110+
* @param {Context} context
111+
* @param {BaseComment[] | undefined} comments
112+
* @param {('leading' | 'trailing')} position
113+
*/
114+
function write_additional_comments(context, comments, position) {
115+
if (!comments) {
116+
return;
117+
}
118+
119+
for (let i = 0; i < comments.length; i += 1) {
120+
const comment = comments[i];
121+
122+
if (position === 'trailing' && i === 0) {
123+
context.write(' ');
124+
}
125+
126+
write_comment(comment, context);
127+
128+
if (position === 'leading') {
129+
if (comment.type === 'Line') {
130+
context.newline();
131+
} else if (comment.type === 'Block' && !comment.value.includes('\n')) {
132+
context.write(' ');
133+
}
134+
}
135+
}
136+
}
137+
107138
/**
108139
* Set `comment_index` to be the first comment after `start`.
109140
* Most of the time this is already correct, but if nodes
@@ -775,11 +806,15 @@ export default (options = {}) => {
775806

776807
return {
777808
_(node, context, visit) {
809+
write_additional_comments(context, options.getLeadingComments?.(node), 'leading');
810+
778811
if (node.loc) {
779812
flush_comments_until(context, null, node.loc.start, true);
780813
}
781814

782815
visit(node);
816+
817+
write_additional_comments(context, options.getTrailingComments?.(node), 'trailing');
783818
},
784819

785820
AccessorProperty:

src/languages/ts/public.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export * from './index';
2-
export { Comment } from '../types';
2+
export { BaseComment, Comment } from '../types';

src/languages/tsx/public.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export * from './index';
2-
export { Comment } from '../types';
2+
export { BaseComment, Comment } from '../types';

src/languages/types.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { TSESTree } from '@typescript-eslint/types';
2+
13
export type TSOptions = {
24
quotes?: 'double' | 'single';
35
comments?: Comment[];
6+
getLeadingComments?: (node: TSESTree.Node) => BaseComment[] | undefined;
7+
getTrailingComments?: (node: TSESTree.Node) => BaseComment[] | undefined;
48
};
59

610
interface Position {
@@ -10,11 +14,14 @@ interface Position {
1014

1115
// this exists in TSESTree but because of the inanity around enums
1216
// it's easier to do this ourselves
13-
export interface Comment {
17+
export interface BaseComment {
1418
type: 'Line' | 'Block';
1519
value: string;
1620
start?: number;
1721
end?: number;
22+
}
23+
24+
export interface Comment extends BaseComment {
1825
loc: {
1926
start: Position;
2027
end: Position;

test/additional-comments.test.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// @ts-check
2+
/** @import { TSESTree } from '@typescript-eslint/types' */
3+
/** @import { BaseComment } from '../src/languages/types.js' */
4+
import { expect, test } from 'vitest';
5+
import { print } from '../src/index.js';
6+
import { acornParse } from './common.js';
7+
import ts from '../src/languages/ts/index.js';
8+
9+
/**
10+
* @param {string} value
11+
* @returns {BaseComment}
12+
*/
13+
function line(value) {
14+
return { type: 'Line', value };
15+
}
16+
17+
/**
18+
* @param {string} value
19+
* @returns {BaseComment}
20+
*/
21+
function block(value) {
22+
return { type: 'Block', value };
23+
}
24+
25+
/**
26+
* Helper to get return statement from a simple function
27+
* @param {TSESTree.Program} ast - Parsed AST
28+
* @returns {TSESTree.Node} The return statement
29+
*/
30+
function get_return_statement(ast) {
31+
const functionDecl = ast.body[0];
32+
// @ts-expect-error accessing function body
33+
const statements = functionDecl.body.body;
34+
// Find the return statement (could be first or second depending on function structure)
35+
return statements.find(/** @param {any} stmt */ (stmt) => stmt.type === 'ReturnStatement');
36+
}
37+
38+
test('additional comments are inserted correctly', () => {
39+
const input = `function example() {
40+
const x = 1;
41+
return x;
42+
}`;
43+
44+
const { ast } = acornParse(input);
45+
const returnStatement = get_return_statement(ast);
46+
expect(returnStatement.type).toBe('ReturnStatement');
47+
48+
const { code } = print(
49+
ast,
50+
ts({
51+
getLeadingComments: (n) =>
52+
n === returnStatement ? [line(' This is a leading comment')] : undefined,
53+
getTrailingComments: (n) =>
54+
n === returnStatement ? [block(' This is a trailing comment ')] : undefined
55+
})
56+
);
57+
58+
expect(code).toContain('// This is a leading comment');
59+
expect(code).toContain('/* This is a trailing comment */');
60+
});
61+
62+
test('only leading comments are inserted when specified', () => {
63+
const input = `function test() { return 42; }`;
64+
const { ast } = acornParse(input);
65+
const returnStatement = get_return_statement(ast);
66+
67+
const { code } = print(
68+
ast,
69+
ts({
70+
getLeadingComments: (n) => (n === returnStatement ? [line(' Leading only ')] : undefined)
71+
})
72+
);
73+
74+
expect(code).toContain('// Leading only');
75+
expect(code).not.toContain('trailing');
76+
});
77+
78+
test('only trailing comments are inserted when specified', () => {
79+
const input = `function test() { return 42; }`;
80+
const { ast } = acornParse(input);
81+
const returnStatement = get_return_statement(ast);
82+
83+
const { code } = print(
84+
ast,
85+
ts({
86+
getTrailingComments: (n) => (n === returnStatement ? [block(' Trailing only ')] : undefined)
87+
})
88+
);
89+
90+
expect(code).toContain('/* Trailing only */');
91+
expect(code).not.toContain('//');
92+
});
93+
94+
test('additional comments multi-line comments have new line', () => {
95+
const input = `function example() {
96+
const x = 1;
97+
return x;
98+
}`;
99+
100+
const { ast } = acornParse(input);
101+
const returnStatement = get_return_statement(ast);
102+
expect(returnStatement.type).toBe('ReturnStatement');
103+
104+
const { code } = print(
105+
ast,
106+
ts({
107+
getLeadingComments: (n) =>
108+
n === returnStatement ? [block('*\n * This is a leading comment\n ')] : undefined
109+
})
110+
);
111+
112+
expect(code).toMatchInlineSnapshot(`
113+
"function example() {
114+
const x = 1;
115+
116+
/**
117+
* This is a leading comment
118+
*/
119+
return x;
120+
}"
121+
`);
122+
});

0 commit comments

Comments
 (0)