Skip to content

Commit 59f7a64

Browse files
author
Ilya Golovin
committed
feat: make addDestruct handle some vue reactivity case
1 parent 4ae4e52 commit 59f7a64

File tree

4 files changed

+174
-42
lines changed

4 files changed

+174
-42
lines changed

typescript/src/codeActions/custom/addDestructure/addSplittedDestructure.ts

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { uniq } from 'rambda'
2-
import { findChildContainingExactPosition, getChangesTracker, getPositionHighlights, isValidInitializerForDestructure, makeUniqueName } from '../../../utils'
1+
import { uniqBy } from 'lodash'
2+
import { getChangesTracker, getPositionHighlights, isValidInitializerForDestructure } from '../../../utils'
3+
import isVueFileName from '../../../utils/vue/isVueFileName'
4+
import { checkNeedToRefsWrap } from './vueSupportUtils'
5+
import { getDestructureReplaceInfo } from './getDestructureReplaceInfo'
36

47
export default (node: ts.Node, sourceFile: ts.SourceFile, formatOptions: ts.FormatCodeSettings | undefined, languageService: ts.LanguageService) => {
58
const isValidInitializer = ts.isVariableDeclaration(node.parent) && node.parent.initializer && isValidInitializerForDestructure(node.parent.initializer)
@@ -12,52 +15,32 @@ export default (node: ts.Node, sourceFile: ts.SourceFile, formatOptions: ts.Form
1215
if (!highlightPositions) return
1316
const tracker = getChangesTracker(formatOptions ?? {})
1417

15-
const propertyNames: Array<{ initial: string; unique: string | undefined }> = []
16-
let nodeToReplaceWithBindingPattern: ts.Identifier | undefined
18+
const res = getDestructureReplaceInfo(highlightPositions, node, sourceFile, languageService)
1719

18-
for (const pos of highlightPositions) {
19-
const highlightedNode = findChildContainingExactPosition(sourceFile, pos)
20+
if (!res) return
2021

21-
if (!highlightedNode) continue
22+
const { propertiesToReplace, nodeToReplaceWithBindingPattern } = res
2223

23-
if (
24-
ts.isElementAccessExpression(highlightedNode.parent) ||
25-
ts.isTypeQueryNode(highlightedNode.parent) ||
26-
(ts.isCallExpression(highlightedNode.parent.parent) && highlightedNode.parent.parent.expression === highlightedNode.parent)
27-
)
28-
return
29-
30-
if (ts.isIdentifier(highlightedNode) && ts.isPropertyAccessExpression(highlightedNode.parent)) {
31-
const accessorName = highlightedNode.parent.name.getText()
24+
if (!nodeToReplaceWithBindingPattern || propertiesToReplace.length === 0) return
3225

33-
if (!accessorName) continue
26+
const shouldHandleVueReactivityLose =
27+
isVueFileName(sourceFile.fileName) &&
28+
ts.isVariableDeclaration(nodeToReplaceWithBindingPattern.parent) &&
29+
nodeToReplaceWithBindingPattern.parent.initializer &&
30+
checkNeedToRefsWrap(nodeToReplaceWithBindingPattern.parent.initializer)
3431

35-
const uniqueName = makeUniqueName(accessorName, node, languageService, sourceFile)
32+
for (const { initial, range, unique } of propertiesToReplace) {
33+
const uniqueNameIdentifier = ts.factory.createIdentifier(unique || initial)
3634

37-
propertyNames.push({ initial: accessorName, unique: uniqueName === accessorName ? undefined : uniqueName })
38-
const range =
39-
ts.isPropertyAssignment(highlightedNode.parent.parent) && highlightedNode.parent.parent.name.getText() === accessorName
40-
? {
41-
pos: highlightedNode.parent.parent.pos + highlightedNode.parent.parent.getLeadingTriviaWidth(),
42-
end: highlightedNode.parent.parent.end,
43-
}
44-
: { pos, end: highlightedNode.parent.end }
45-
46-
tracker.replaceRangeWithText(sourceFile, range, uniqueName)
47-
continue
48-
}
49-
50-
if (ts.isIdentifier(highlightedNode) && (ts.isVariableDeclaration(highlightedNode.parent) || ts.isParameter(highlightedNode.parent))) {
51-
// Already met a target node - abort as we encountered direct use of the potential destructured variable
52-
if (nodeToReplaceWithBindingPattern) return
53-
nodeToReplaceWithBindingPattern = highlightedNode
35+
if (shouldHandleVueReactivityLose) {
36+
const propertyAccessExpression = ts.factory.createPropertyAccessExpression(uniqueNameIdentifier, 'value')
37+
tracker.replaceRange(sourceFile, range, propertyAccessExpression)
5438
continue
5539
}
40+
tracker.replaceRange(sourceFile, range, uniqueNameIdentifier)
5641
}
5742

58-
if (!nodeToReplaceWithBindingPattern || propertyNames.length === 0) return
59-
60-
const bindings = uniq(propertyNames).map(({ initial, unique }) => {
43+
const bindings = uniqBy(propertiesToReplace, 'unique').map(({ initial, unique }) => {
6144
return ts.factory.createBindingElement(undefined, unique ? initial : undefined, unique ?? initial)
6245
})
6346

@@ -73,6 +56,22 @@ export default (node: ts.Node, sourceFile: ts.SourceFile, formatOptions: ts.Form
7356
bindingPattern,
7457
)
7558

59+
if (shouldHandleVueReactivityLose) {
60+
// Wrap the `defineProps` with `toRefs`
61+
const toRefs = ts.factory.createIdentifier('toRefs')
62+
const unwrappedCall = nodeToReplaceWithBindingPattern.parent.initializer
63+
const wrappedWithToRefsCall = ts.factory.createCallExpression(toRefs, undefined, [unwrappedCall])
64+
65+
tracker.replaceRange(
66+
sourceFile,
67+
{
68+
pos: unwrappedCall.pos,
69+
end: unwrappedCall.end,
70+
},
71+
wrappedWithToRefsCall,
72+
)
73+
}
74+
7675
const changes = tracker.getChanges()
7776
if (!changes) return undefined
7877
return {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { findChildContainingExactPosition, makeUniqueName } from '../../../utils'
2+
3+
export const getDestructureReplaceInfo = (highlightPositions: number[], node: ts.Node, sourceFile: ts.SourceFile, languageService: ts.LanguageService) => {
4+
const propertiesToReplace: Array<{ initial: string; unique: string | undefined; range: { pos: number; end: number } }> = []
5+
let nodeToReplaceWithBindingPattern: ts.Identifier | undefined
6+
7+
for (const pos of highlightPositions) {
8+
const highlightedNode = findChildContainingExactPosition(sourceFile, pos)
9+
10+
if (!highlightedNode) continue
11+
12+
if (
13+
ts.isElementAccessExpression(highlightedNode.parent) ||
14+
ts.isTypeQueryNode(highlightedNode.parent) ||
15+
(ts.isCallExpression(highlightedNode.parent.parent) && highlightedNode.parent.parent.expression === highlightedNode.parent)
16+
)
17+
return
18+
19+
if (ts.isIdentifier(highlightedNode) && ts.isPropertyAccessExpression(highlightedNode.parent)) {
20+
const accessorName = highlightedNode.parent.name.getText()
21+
22+
if (!accessorName) continue
23+
24+
const uniqueName = makeUniqueName(accessorName, node, languageService, sourceFile)
25+
26+
const range =
27+
ts.isPropertyAssignment(highlightedNode.parent.parent) && highlightedNode.parent.parent.name.getText() === accessorName
28+
? {
29+
pos: highlightedNode.parent.parent.pos + highlightedNode.parent.parent.getLeadingTriviaWidth(),
30+
end: highlightedNode.parent.parent.end,
31+
}
32+
: { pos, end: highlightedNode.parent.end }
33+
34+
propertiesToReplace.push({ initial: accessorName, unique: uniqueName === accessorName ? undefined : uniqueName, range })
35+
36+
continue
37+
}
38+
39+
if (ts.isIdentifier(highlightedNode) && (ts.isVariableDeclaration(highlightedNode.parent) || ts.isParameter(highlightedNode.parent))) {
40+
// Already met a target node - abort as we encountered direct use of the potential destructured variable
41+
if (nodeToReplaceWithBindingPattern) return
42+
nodeToReplaceWithBindingPattern = highlightedNode
43+
continue
44+
}
45+
}
46+
return { propertiesToReplace, nodeToReplaceWithBindingPattern }
47+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { findChildContainingExactPosition } from '../../../utils'
2+
3+
export const checkAutoInsertDotValue = (sourceFile: ts.SourceFile, position: number, languageService: ts.LanguageService) => {
4+
const node = findChildContainingExactPosition(sourceFile, position)
5+
if (!node || isBlacklistNode(sourceFile, position)) return false
6+
7+
const checker = languageService.getProgram()!.getTypeChecker()
8+
const type = checker.getTypeAtLocation(node)
9+
const props = type.getProperties()
10+
11+
if (props.some(prop => prop.name === 'value')) return true
12+
return false
13+
}
14+
/**
15+
* Checks if the given expression needs to be wrapped with `toRefs` to preserve reactivity.
16+
* @param expression The expression to check.
17+
* @returns A boolean value indicating whether the expression needs to be wrapped.
18+
*/
19+
export const checkNeedToRefsWrap = (expression: ts.Expression) => {
20+
const willLoseReactivityIfDestructFns = new Set(['defineProps', 'reactive'])
21+
return Boolean(ts.isCallExpression(expression) && ts.isIdentifier(expression.expression) && willLoseReactivityIfDestructFns.has(expression.expression.text))
22+
}
23+
24+
function isBlacklistNode(node: ts.Node, pos: number) {
25+
if (ts.isVariableDeclaration(node) && pos >= node.name.getFullStart() && pos <= node.name.getEnd()) {
26+
return true
27+
}
28+
if (ts.isFunctionDeclaration(node) && node.name && pos >= node.name.getFullStart() && pos <= node.name.getEnd()) {
29+
return true
30+
}
31+
if (ts.isParameter(node) && pos >= node.name.getFullStart() && pos <= node.name.getEnd()) {
32+
return true
33+
}
34+
if (ts.isPropertyAssignment(node) && pos >= node.name.getFullStart() && pos <= node.name.getEnd()) {
35+
return true
36+
}
37+
if (ts.isShorthandPropertyAssignment(node)) {
38+
return true
39+
}
40+
if (ts.isImportDeclaration(node)) {
41+
return true
42+
}
43+
if (ts.isLiteralTypeNode(node)) {
44+
return true
45+
}
46+
if (ts.isTypeReferenceNode(node)) {
47+
return true
48+
}
49+
if (ts.isPropertyAccessExpression(node) && node.expression.end === pos && node.name.text === 'value') {
50+
return true
51+
}
52+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && isWatchOrUseFunction(node.expression.text) && isTopLevelArgOrArrayTopLevelItem(node)) {
53+
return true
54+
}
55+
56+
let _isBlacklistNode = false
57+
node.forEachChild(node => {
58+
if (_isBlacklistNode) return
59+
if (pos >= node.getFullStart() && pos <= node.getEnd() && isBlacklistNode(node, pos)) {
60+
_isBlacklistNode = true
61+
}
62+
})
63+
return _isBlacklistNode
64+
65+
function isWatchOrUseFunction(fnName: string) {
66+
return fnName === 'watch' || fnName === 'unref' || fnName === 'triggerRef' || fnName === 'isRef' || fnName.startsWith('use-')
67+
}
68+
function isTopLevelArgOrArrayTopLevelItem(node: ts.CallExpression) {
69+
for (const arg of node.arguments) {
70+
if (pos >= arg.getFullStart() && pos <= arg.getEnd()) {
71+
if (ts.isIdentifier(arg)) {
72+
return true
73+
}
74+
if (ts.isArrayLiteralExpression(arg)) {
75+
for (const el of arg.elements) {
76+
if (pos >= el.getFullStart() && pos <= el.getEnd()) {
77+
return ts.isIdentifier(el)
78+
}
79+
}
80+
}
81+
return false
82+
}
83+
}
84+
return false
85+
}
86+
}

typescript/src/codeActions/getCodeActions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import changeStringReplaceToRegex from './custom/changeStringReplaceToRegex'
88
import splitDeclarationAndInitialization from './custom/splitDeclarationAndInitialization'
99
import declareMissingProperties from './extended/declareMissingProperties'
1010
import { renameParameterToNameFromType, renameAllParametersToNameFromType } from './custom/renameParameterToNameFromType'
11-
import addDestructure_1 from './custom/addDestructure/addDestructure'
12-
import fromDestructure_1 from './custom/fromDestructure/fromDestructure'
11+
import addDestructure from './custom/addDestructure/addDestructure'
12+
import fromDestructure from './custom/fromDestructure/fromDestructure'
1313

1414
const codeActions: CodeAction[] = [
15-
addDestructure_1,
16-
fromDestructure_1,
15+
addDestructure,
16+
fromDestructure,
1717
objectSwapKeysAndValues,
1818
changeStringReplaceToRegex,
1919
splitDeclarationAndInitialization,

0 commit comments

Comments
 (0)