Skip to content

Commit 0b1c13f

Browse files
authored
feat(refactoring): add/from destructure bug fixes, this keyword support (#183)
1 parent b7352ee commit 0b1c13f

21 files changed

+950
-924
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"fs-extra": "^10.1.0",
139139
"got": "^12.5.3",
140140
"got-cjs": "npm:got@^11.x",
141+
"prettier": "3.1.0",
141142
"tsm": "^2.3.0",
142143
"type-fest": "^2.13.1",
143144
"typed-jsonfile": "^0.2.1",

pnpm-lock.yaml

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typescript/src/codeActions/custom/addDestructure.ts

-174
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { getChangesTracker, isValidInitializerForDestructure } from '../../../utils'
2+
import { CodeAction } from '../../getCodeActions'
3+
import createDestructuredDeclaration from './createDestructuredDeclaration'
4+
import addSplittedDestructure from './addSplittedDestructure'
5+
6+
export default {
7+
id: 'addDestruct',
8+
name: 'Add Destruct',
9+
kind: 'refactor.rewrite.add-destruct',
10+
tryToApply(sourceFile, position, _range, node, formatOptions, languageService) {
11+
if (!node || !position) return
12+
const initialDeclaration = ts.findAncestor(node, n => ts.isVariableDeclaration(n)) as ts.VariableDeclaration | undefined
13+
14+
if (initialDeclaration && !ts.isObjectBindingPattern(initialDeclaration.name)) {
15+
const { initializer, type, name } = initialDeclaration
16+
17+
const result = addSplittedDestructure(node, sourceFile, formatOptions, languageService)
18+
19+
if (result) return result
20+
21+
if (!initializer || !isValidInitializerForDestructure(initializer)) return
22+
23+
const tracker = getChangesTracker(formatOptions ?? {})
24+
const createdDeclaration = createDestructuredDeclaration(initializer, type, name)
25+
if (createdDeclaration) {
26+
tracker.replaceRange(
27+
sourceFile,
28+
{
29+
pos: initialDeclaration.pos + initialDeclaration.getLeadingTriviaWidth(),
30+
end: initialDeclaration.end,
31+
},
32+
createdDeclaration,
33+
)
34+
35+
const changes = tracker.getChanges()
36+
if (!changes) return undefined
37+
return {
38+
edits: [
39+
{
40+
fileName: sourceFile.fileName,
41+
textChanges: changes[0]!.textChanges,
42+
},
43+
],
44+
}
45+
}
46+
}
47+
return addSplittedDestructure(node, sourceFile, formatOptions, languageService)
48+
},
49+
} satisfies CodeAction
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { findChildContainingExactPosition, getChangesTracker, getPositionHighlights, isValidInitializerForDestructure, makeUniqueName } from '../../../utils'
2+
3+
export default (node: ts.Node, sourceFile: ts.SourceFile, formatOptions: ts.FormatCodeSettings | undefined, languageService: ts.LanguageService) => {
4+
const isValidInitializer = ts.isVariableDeclaration(node.parent) && node.parent.initializer && isValidInitializerForDestructure(node.parent.initializer)
5+
6+
// Make sure it only triggers on the destructuring object or parameter
7+
if (!ts.isIdentifier(node) || !(isValidInitializer || ts.isParameter(node.parent))) return
8+
9+
const highlightPositions = getPositionHighlights(node.getStart(), sourceFile, languageService)
10+
11+
if (!highlightPositions) return
12+
const tracker = getChangesTracker(formatOptions ?? {})
13+
14+
const propertyNames: Array<{ initial: string; unique: string | undefined }> = []
15+
let nodeToReplaceWithBindingPattern: ts.Identifier | undefined
16+
17+
for (const pos of highlightPositions) {
18+
const highlightedNode = findChildContainingExactPosition(sourceFile, pos)
19+
20+
if (!highlightedNode) continue
21+
22+
if (
23+
ts.isElementAccessExpression(highlightedNode.parent) ||
24+
ts.isCallExpression(highlightedNode.parent.parent) ||
25+
ts.isTypeQueryNode(highlightedNode.parent)
26+
)
27+
return
28+
29+
if (ts.isIdentifier(highlightedNode) && ts.isPropertyAccessExpression(highlightedNode.parent)) {
30+
const accessorName = highlightedNode.parent.name.getText()
31+
32+
if (!accessorName) continue
33+
34+
const uniqueName = makeUniqueName(accessorName, node, languageService, sourceFile)
35+
36+
propertyNames.push({ initial: accessorName, unique: uniqueName === accessorName ? undefined : uniqueName })
37+
const range =
38+
ts.isPropertyAssignment(highlightedNode.parent.parent) && highlightedNode.parent.parent.name.getText() === accessorName
39+
? {
40+
pos: highlightedNode.parent.parent.pos + highlightedNode.parent.parent.getLeadingTriviaWidth(),
41+
end: highlightedNode.parent.parent.end,
42+
}
43+
: { pos, end: highlightedNode.parent.end }
44+
45+
tracker.replaceRangeWithText(sourceFile, range, uniqueName)
46+
continue
47+
}
48+
49+
if (ts.isIdentifier(highlightedNode) && (ts.isVariableDeclaration(highlightedNode.parent) || ts.isParameter(highlightedNode.parent))) {
50+
// Already met a target node - abort as we encountered direct use of the potential destructured variable
51+
if (nodeToReplaceWithBindingPattern) return
52+
nodeToReplaceWithBindingPattern = highlightedNode
53+
continue
54+
}
55+
}
56+
57+
if (!nodeToReplaceWithBindingPattern || propertyNames.length === 0) return
58+
59+
const bindings = propertyNames.map(({ initial, unique }) => {
60+
return ts.factory.createBindingElement(undefined, unique ? initial : undefined, unique ?? initial)
61+
})
62+
63+
const bindingPattern = ts.factory.createObjectBindingPattern(bindings)
64+
const { pos, end } = nodeToReplaceWithBindingPattern
65+
66+
tracker.replaceRange(
67+
sourceFile,
68+
{
69+
pos: pos + nodeToReplaceWithBindingPattern.getLeadingTriviaWidth(),
70+
end,
71+
},
72+
bindingPattern,
73+
)
74+
75+
const changes = tracker.getChanges()
76+
if (!changes) return undefined
77+
return {
78+
edits: [
79+
{
80+
fileName: sourceFile.fileName,
81+
textChanges: changes[0]!.textChanges,
82+
},
83+
],
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export default (initializer: ts.Expression, type: ts.TypeNode | undefined, declarationName: ts.BindingName) => {
2+
if (!ts.isPropertyAccessExpression(initializer)) return
3+
4+
const propertyName = initializer.name.text
5+
const { factory } = ts
6+
7+
const bindingElement = factory.createBindingElement(
8+
undefined,
9+
declarationName.getText() === propertyName ? undefined : propertyName,
10+
declarationName.getText(),
11+
)
12+
13+
return factory.createVariableDeclaration(
14+
factory.createObjectBindingPattern([bindingElement]),
15+
undefined,
16+
type ? factory.createTypeLiteralNode([factory.createPropertySignature(undefined, factory.createIdentifier(propertyName), undefined, type)]) : undefined,
17+
initializer.expression,
18+
)
19+
}

0 commit comments

Comments
 (0)