-
Notifications
You must be signed in to change notification settings - Fork 6
/
code-parser.ts
151 lines (129 loc) · 3.7 KB
/
code-parser.ts
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
const recast = require('recast');
const types = require('ast-types');
const typeBuilders = require('ast-types').builders;
// the name of the global that holds all the values in the
// injected script
const AllVarsVariableName = '__AllVars';
let previousCode = null;
export function codeHasChanged(userCode: string): boolean {
return detectCodeChanges(astFromUserCode(userCode).program.body, previousCode.program.body);
}
/**
* Receives:
* let a = 1;
*
* and returns:
* const __AllVars = {
* aHash: 1
* }
* let a = __AllVars['aHash'];
*/
export function parseCode(userCode: string): string {
try {
const vars = {};
const ast = astFromUserCode(userCode);
types.visit(ast, {
visitLiteral: (path) => {
const key = nodeToKey(path, vars);
vars[key] = path.value.value;
path.replace(
typeBuilders.memberExpression(
typeBuilders.identifier(AllVarsVariableName),
typeBuilders.identifier(key)));
return false;
}
});
const modifiedUserCode = recast.prettyPrint(ast).code;
previousCode = astFromUserCode(userCode);
return `const ${AllVarsVariableName} = ${JSON.stringify(vars)}; ${modifiedUserCode}`;
} catch (e) {
return parseCode(recast.prettyPrint(previousCode));
}
}
/**
* Receives:
* let a = 1;
*
* and returns:
* {
* aHash: 1
* }
*/
export function getVars(userCode: string): any {
const vars = {};
const ast = astFromUserCode(userCode);
types.visit(ast, {
visitLiteral: (path) => {
const key = nodeToKey(path, vars);
vars[key] = path.value.value;
return false;
}
});
return vars;
}
/**
* Returns the normalised ast ignoring user formatting.
*/
function astFromUserCode(userCode: string): any {
return recast.parse(recast.prettyPrint(recast.parse(userCode)).code);
}
/**
* Returns a uniquely identifiable key given an AST node.
*/
function nodeToKey(path: any, vars: any): string {
let key = 'a' + path.value.loc.start.line;
let count = 1;
while (key in vars) {
key = 'a' + path.value.loc.start.line + '_' + count++;
}
return key;
}
/**
* Here be dragons.
* Tries to detect recursively if one AST is different from another, ignoring
* literal value changes.
*/
function detectCodeChanges(actual: any, expected: any): boolean {
// this returns true when comparing base types (strings, numbers, booleans)
// we reach this case in many properties like an function's name.
if (Object(actual) !== actual) {
return actual !== expected;
}
if (Array.isArray(actual)) {
if (actual.length !== expected.length) {
return true;
}
for (let i = 0; i < actual.length; i++) {
if (detectCodeChanges(actual[i], expected[i])) {
return true;
}
}
return false;
}
const actualIsLiteral = actual.type === 'Literal';
const expectedIsLiteral = expected.type === 'Literal';
if (actualIsLiteral && expectedIsLiteral) {
return false;
} else if (!actualIsLiteral && !expectedIsLiteral) {
for (let attr in actual) {
/**
* Sadly there's no other way to compare AST nodes without treating each type specifically,
* as there's no common interface.
*
* This code simply iterates through all object properties and compares them. `loc`, however
* is a property that nodes have that can differ between `actual` and `expected`, but we don't
* necessarily care for this change as it might just be a literal value changing.
*/
if (expected && attr in expected) {
if (attr !== 'loc' && detectCodeChanges(actual[attr], expected[attr])) {
return true;
}
} else {
return true;
}
}
} else {
return true;
}
return false;
}