-
Notifications
You must be signed in to change notification settings - Fork 36
/
resolveMemberExpressionsLocalReferences.js
79 lines (78 loc) · 3.52 KB
/
resolveMemberExpressionsLocalReferences.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import {evalInVm} from '../utils/evalInVm.js';
import {badValue, skipProperties} from '../config.js';
import {createOrderedSrc} from '../utils/createOrderedSrc.js';
import {areReferencesModified} from '../utils/areReferencesModified.js';
import {getDeclarationWithContext} from '../utils/getDeclarationWithContext.js';
import {getMainDeclaredObjectOfMemberExpression} from '../utils/getMainDeclaredObjectOfMemberExpression.js';
/**
* Resolve member expressions to the value they stand for, if they're defined in the script.
* E.g.
* const a = [1, 2, 3];
* const b = a[2]; // <-- will be resolved to 3
* const c = 0;
* const d = a[c]; // <-- will be resolved to 1
* ---
* const a = {hello: 'world'};
* const b = a['hello']; // <-- will be resolved to 'world'
* @param {Arborist} arb
* @param {Function} candidateFilter (optional) a filter to apply on the candidates list
* @return {Arborist}
*/
export default function resolveMemberExpressionsLocalReferences(arb, candidateFilter = () => true) {
for (let i = 0; i < arb.ast.length; i++) {
const n = arb.ast[i];
if (n.type === 'MemberExpression' &&
['Identifier', 'Literal'].includes(n.property.type) &&
!skipProperties.includes(n.property?.name || n.property?.value) &&
(!(n.parentKey === 'left' && n.parentNode.type === 'AssignmentExpression')) &&
candidateFilter(n)) {
// If this member expression is the callee of a call expression - skip it
if (n.parentNode.type === 'CallExpression' && n.parentKey === 'callee') continue;
// If this member expression is a part of another member expression - get the first parentNode
// which has a declaration in the code;
// E.g. a.b[c.d] --> if candidate is c.d, the c identifier will be selected;
// a.b.c.d --> if the candidate is c.d, the 'a' identifier will be selected;
let relevantIdentifier = getMainDeclaredObjectOfMemberExpression(n);
if (relevantIdentifier && relevantIdentifier.declNode) {
// Skip if the relevant identifier is on the left side of an assignment.
if (relevantIdentifier.parentNode.parentNode.type === 'AssignmentExpression' &&
relevantIdentifier.parentNode.parentKey === 'left') continue;
const declNode = relevantIdentifier.declNode;
// Skip if the identifier was declared as a function's parameter.
if (/Function/.test(declNode.parentNode.type) &&
(declNode.parentNode.params || []).find(p => p === declNode)) continue;
const prop = n.property;
if (prop.type === 'Identifier' && prop.declNode?.references && areReferencesModified(arb.ast, prop.declNode.references)) continue;
const context = createOrderedSrc(getDeclarationWithContext(relevantIdentifier.declNode.parentNode));
if (context) {
const src = `${context}\n${n.src}`;
const replacementNode = evalInVm(src);
if (replacementNode !== badValue) {
let isEmptyReplacement = false;
switch (replacementNode.type) {
case 'ArrayExpression':
if (!replacementNode.elements.length) isEmptyReplacement = true;
break;
case 'ObjectExpression':
if (!replacementNode.properties.length) isEmptyReplacement = true;
break;
case 'Literal':
if (
!String(replacementNode.value).length || // ''
replacementNode.raw === 'null' // null
) isEmptyReplacement = true;
break;
case 'Identifier':
if (replacementNode.name === 'undefined') isEmptyReplacement = true;
break;
}
if (!isEmptyReplacement) {
arb.markNode(n, replacementNode);
}
}
}
}
}
}
return arb;
}