Skip to content

Commit 625bcb5

Browse files
committed
fix: preserve comments in replacement patterns
1 parent 75a85ee commit 625bcb5

11 files changed

+148
-50
lines changed

src/backend/parse.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Backend } from './Backend'
22
import { NodePath, Expression, Statement, Node, Comment } from '../types'
33
import ensureArray from '../util/ensureArray'
44
import forEachNode from '../util/forEachNode'
5+
import transferComments from '../util/transferComments'
56

67
function parse0(
78
backend: Backend,
@@ -14,7 +15,11 @@ function parse0(
1415
return backend.template.expression(strings, ...quasis)
1516
if (result.length > 1) return result
1617
const node = result[0]
17-
return node.type === 'ExpressionStatement' ? node.expression : node
18+
if (node.type === 'ExpressionStatement') {
19+
transferComments(node, node.expression, { leading: true, trailing: true })
20+
return node.expression
21+
}
22+
return node
1823
} catch (error) {
1924
// fallthrough
2025
}

src/compileReplacement/ExportNamedDeclaration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ReplaceableMatch,
66
} from '.'
77
import compileGenericNodeReplacement from './GenericNodeReplacement'
8+
import transferComments from '../util/transferComments'
89

910
export default function compileExportNamedDeclarationReplacement(
1011
path: NodePath<ExportNamedDeclaration, ExportNamedDeclaration>,
@@ -27,6 +28,7 @@ export default function compileExportNamedDeclarationReplacement(
2728
)
2829
}
2930
}
31+
transferComments(path.node, result)
3032
return result
3133
},
3234
}

src/compileReplacement/GenericNodeReplacement.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import compileReplacement, {
55
ReplaceableMatch,
66
} from './index'
77
import indentDebug from '../compileMatcher/indentDebug'
8+
import transferComments from '../util/transferComments'
89

910
export default function compileGenericNodeReplacement(
1011
path: NodePath,
@@ -53,6 +54,12 @@ export default function compileGenericNodeReplacement(
5354
if (value !== undefined) result[key] = value
5455
}
5556

57+
transferComments(pattern, result, {
58+
leading: true,
59+
trailing: true,
60+
clone: true,
61+
})
62+
5663
return result
5764
},
5865
}

src/compileReplacement/ImportDeclaration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ReplaceableMatch,
66
} from '.'
77
import compileGenericNodeReplacement from './GenericNodeReplacement'
8+
import transferComments from '../util/transferComments'
89

910
export default function compileImportDeclarationReplacement(
1011
path: NodePath<ImportDeclaration, ImportDeclaration>,
@@ -27,6 +28,7 @@ export default function compileImportDeclarationReplacement(
2728
)
2829
}
2930
}
31+
transferComments(path.node, result)
3032
return result
3133
},
3234
}

src/compileReplacement/Placeholder.ts

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '../compileMatcher/Placeholder'
1414
import createReplacementConverter, { bulkConvert } from '../convertReplacement'
1515
import cloneNode from '../util/cloneNode'
16+
import transferComments from '../util/transferComments'
1617
export { unescapeIdentifier }
1718

1819
export function compileArrayPlaceholderReplacement(
@@ -25,18 +26,25 @@ export function compileArrayPlaceholderReplacement(
2526
getArrayPlaceholder(identifier) || getRestPlaceholder(identifier)
2627
if (arrayPlaceholder && isCapturePlaceholder(arrayPlaceholder)) {
2728
const convertReplacement = createReplacementConverter(pattern)
29+
30+
const baseGenerate = (match: ReplaceableMatch): Node | Node[] => {
31+
const captures = match.arrayCaptures?.[arrayPlaceholder]
32+
if (captures) {
33+
return [
34+
...bulkConvert(
35+
captures.map((c) => cloneNode(c)),
36+
convertReplacement
37+
),
38+
]
39+
}
40+
return [...bulkConvert(cloneNode(pattern.value), convertReplacement)]
41+
}
42+
2843
return {
2944
generate: (match: ReplaceableMatch): Node | Node[] => {
30-
const captures = match.arrayCaptures?.[arrayPlaceholder]
31-
if (captures) {
32-
return [
33-
...bulkConvert(
34-
captures.map((c) => cloneNode(c)),
35-
convertReplacement
36-
),
37-
]
38-
}
39-
return [...bulkConvert(cloneNode(pattern.value), convertReplacement)]
45+
const result = baseGenerate(match)
46+
transferComments(pattern.node, result)
47+
return result
4048
},
4149
}
4250
}
@@ -50,16 +58,22 @@ export default function compilePlaceholderReplacement(
5058
const placeholder = getPlaceholder(identifier)
5159
if (placeholder && isCapturePlaceholder(placeholder)) {
5260
const convertReplacement = createReplacementConverter(pattern)
61+
const baseGenerate = (match: ReplaceableMatch): Node | Node[] => {
62+
const capture = match.captures?.[placeholder]
63+
if (capture) {
64+
const clone = cloneNode(capture)
65+
const astx = getAstxMatchInfo(capture)
66+
if (astx?.subcapture) return convertReplacement(astx.subcapture)
67+
return convertReplacement(clone)
68+
}
69+
return convertReplacement(cloneNode(pattern.value))
70+
}
71+
5372
return {
5473
generate: (match: ReplaceableMatch): Node | Node[] => {
55-
const capture = match.captures?.[placeholder]
56-
if (capture) {
57-
const clone = cloneNode(capture)
58-
const astx = getAstxMatchInfo(capture)
59-
if (astx?.subcapture) return convertReplacement(astx.subcapture)
60-
return convertReplacement(clone)
61-
}
62-
return convertReplacement(cloneNode(pattern.value))
74+
const result = baseGenerate(match)
75+
transferComments(pattern.node, result)
76+
return result
6377
},
6478
}
6579
}

src/compileReplacement/StringLiteral.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CompiledReplacement, ReplaceableMatch } from './'
44
import { unescapeIdentifier } from './Placeholder'
55
import cloneNode from '../util/cloneNode'
66
import * as t from '@babel/types'
7+
import transferComments from '../util/transferComments'
78

89
export default function compileStringLiteralReplacement(
910
path: NodePath<StringLiteral>
@@ -14,7 +15,9 @@ export default function compileStringLiteralReplacement(
1415
return {
1516
generate: (match: ReplaceableMatch): StringLiteral => {
1617
const captured = match.stringCaptures?.[placeholder]
17-
return captured ? t.stringLiteral(captured) : cloneNode(pattern)
18+
const result = captured ? t.stringLiteral(captured) : cloneNode(pattern)
19+
transferComments(pattern, result)
20+
return result
1821
},
1922
}
2023
}

src/compileReplacement/TemplateLiteral.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CompiledReplacement, ReplaceableMatch } from './'
44
import { unescapeIdentifier } from './Placeholder'
55
import cloneNode from '../util/cloneNode'
66
import * as t from '@babel/types'
7+
import transferComments from '../util/transferComments'
78

89
function generateValue(cooked: string): { raw: string; cooked: string } {
910
return { raw: cooked.replace(/\\|`|\${/g, '\\$&'), cooked }
@@ -21,12 +22,14 @@ export default function compileTemplateLiteralReplacement(
2122
return {
2223
generate: (match: ReplaceableMatch): TemplateLiteral => {
2324
const captured = match.stringCaptures?.[placeholder]
24-
return captured
25+
const result = captured
2526
? t.templateLiteral(
2627
[t.templateElement(generateValue(captured), true)],
2728
[]
2829
)
2930
: cloneNode(pattern)
31+
transferComments(pattern, result)
32+
return result
3033
},
3134
}
3235
}

src/replace.ts

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import pipeline from './util/pipeline'
77
import lodash from 'lodash'
88
const { last } = lodash
99
import { SimpleReplacementInterface } from './util/SimpleReplacementCollector'
10+
import transferComments from './util/transferComments'
1011

1112
export type ReplaceOptions = {
1213
backend: Backend
@@ -99,8 +100,8 @@ function doReplace(match: Match, replacements: Node[]) {
99100
)
100101

101102
if (replacements.length) {
102-
transferComments(replacedPaths[0], replacements[0], { leading: true })
103-
transferComments(last(replacedPaths), last(replacements), {
103+
transferComments(replacedPaths[0]?.node, replacements[0], { leading: true })
104+
transferComments(last(replacedPaths)?.node, last(replacements), {
104105
trailing: true,
105106
})
106107
replacedPaths[0]?.replace(...replacements)
@@ -110,30 +111,3 @@ function doReplace(match: Match, replacements: Node[]) {
110111
replacedPaths[i].prune()
111112
}
112113
}
113-
114-
function transferComments(
115-
from: NodePath | undefined,
116-
to: Node | undefined,
117-
options: { leading?: boolean; trailing?: boolean }
118-
) {
119-
if (!from || !to) return
120-
const node: any = from.node
121-
const leading = options.leading
122-
? node.comments?.filter((c: any) => c.leading) || node.leadingComments
123-
: undefined
124-
if (leading?.length) {
125-
const dest = node.comments
126-
? (to as any).comments || ((to as any).comments = [])
127-
: (to as any).leadingComments || ((to as any).leadingComments = [])
128-
for (const c of leading) dest.push(c)
129-
}
130-
const trailing = options.trailing
131-
? node.comments?.filter((c: any) => c.trailing) || node.trailingComments
132-
: undefined
133-
if (trailing?.length) {
134-
const dest = node.comments
135-
? (to as any).comments || ((to as any).comments = [])
136-
: (to as any).trailingComments || ((to as any).trailingComments = [])
137-
for (const c of trailing) dest.push(c)
138-
}
139-
}

src/util/transferComments.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Comment, Node } from '../types'
2+
3+
type Commentable = {
4+
comments?: (Comment & { leading?: boolean; trailing?: boolean })[]
5+
leadingComments?: Comment[]
6+
trailingComments?: Comment[]
7+
}
8+
9+
export default function transferComments(
10+
_from: Node | undefined,
11+
_to: Node | Node[] | undefined,
12+
options: { leading?: boolean; trailing?: boolean; clone?: boolean } = {
13+
leading: true,
14+
trailing: true,
15+
clone: true,
16+
}
17+
) {
18+
if (!_from || !_to) return
19+
const from: Commentable = _from as any
20+
const leading = options.leading
21+
? from.comments?.filter((c: any) => c.leading) || from.leadingComments
22+
: undefined
23+
if (leading?.length) {
24+
const to: Commentable = (Array.isArray(_to) ? _to[0] : _to) as any
25+
const dest = from.comments
26+
? to.comments || (to.comments = [])
27+
: to.leadingComments || (to.leadingComments = [])
28+
for (const c of leading) dest.push(options.clone ? cloneComment(c) : c)
29+
}
30+
const trailing = options.trailing
31+
? from.comments?.filter((c: any) => c.trailing) || from.trailingComments
32+
: undefined
33+
if (trailing?.length) {
34+
const to: Commentable = (
35+
Array.isArray(_to) ? _to[_to.length - 1] : _to
36+
) as any
37+
const dest = from.comments
38+
? to.comments || (to.comments = [])
39+
: to.trailingComments || (to.trailingComments = [])
40+
for (const c of trailing) dest.push(options.clone ? cloneComment(c) : c)
41+
}
42+
}
43+
44+
function cloneComment(c: Comment): Comment {
45+
const { type, value } = c
46+
return { type, value }
47+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { findReplaceTestcase } from '../findReplaceTestcase'
2+
import dedent from 'dedent-js'
3+
4+
findReplaceTestcase({
5+
file: __filename,
6+
input: dedent`
7+
const a = 1 + 2
8+
`,
9+
find: dedent`
10+
$a + $b
11+
`,
12+
replace: dedent`
13+
// this is a test
14+
$b + $a
15+
`,
16+
expectedReplace: dedent`
17+
const a = 2 + 1 // this is a test
18+
`,
19+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { findReplaceTestcase } from '../findReplaceTestcase'
2+
import dedent from 'dedent-js'
3+
4+
findReplaceTestcase({
5+
file: __filename,
6+
input: dedent`
7+
t.number()
8+
t.string('foo')
9+
`,
10+
find: dedent`
11+
t.string('$s')
12+
`,
13+
replace: dedent`
14+
// this is a test
15+
$s
16+
`,
17+
expectedReplace: dedent`
18+
t.number()
19+
// this is a test
20+
'foo'
21+
`,
22+
})

0 commit comments

Comments
 (0)