forked from ampproject/amphtml
/
AmpPass.java
192 lines (173 loc) · 6.52 KB
/
AmpPass.java
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ampproject;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.HotSwapCompilerPass;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
/**
* Does a `stripTypeSuffix` which currently can't be done through
* the normal `strip` mechanisms provided by closure compiler.
* Some of the known mechanisms we tried before writing our own compiler pass
* are setStripTypes, setStripTypePrefixes, setStripNameSuffixes, setStripNamePrefixes.
* The normal mechanisms found in closure compiler can't strip the expressions we want because
* they are either prefix based and/or operate on the es6 translated code which would mean they
* operate on a qualifier string name that looks like
* "module$__$__$__$extensions$amp_test$0_1$log.dev.fine".
*
* Other custom pass examples found inside closure compiler src:
* https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/PolymerPass.java
* https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/AngularPass.java
*/
class AmpPass extends AbstractPostOrderCallback implements HotSwapCompilerPass {
final AbstractCompiler compiler;
private final Set<String> stripTypeSuffixes;
final boolean isProd;
public AmpPass(AbstractCompiler compiler, boolean isProd, Set<String> stripTypeSuffixes) {
this.compiler = compiler;
this.stripTypeSuffixes = stripTypeSuffixes;
this.isProd = isProd;
}
@Override public void process(Node externs, Node root) {
hotSwapScript(root, null);
}
@Override public void hotSwapScript(Node scriptRoot, Node originalRoot) {
NodeTraversal.traverseEs6(compiler, scriptRoot, this);
}
@Override public void visit(NodeTraversal t, Node n, Node parent) {
// Remove `dev.assert` calls and preserve first argument if any.
if (isNameStripType(n, ImmutableSet.of( "dev.assert"))) {
maybeEliminateCallExceptFirstParam(n, parent);
// Remove any `stripTypes` passed in outright like `dev.warn`.
} else if (isNameStripType(n, stripTypeSuffixes)) {
removeExpression(n, parent);
// Remove any `getMode().localDev` and `getMode().test` calls and replace it with `false`.
} else if (isProd && isFunctionInvokeAndPropAccess(n, "$mode.getMode",
ImmutableSet.of("localDev", "test"))) {
replaceWithBooleanExpression(false, n, parent);
// Remove any `getMode().minified` calls and replace it with `true`.
} else if (isProd && isFunctionInvokeAndPropAccess(n, "$mode.getMode",
ImmutableSet.of("minified"))) {
replaceWithBooleanExpression(true, n, parent);
}
}
/**
* Predicate for any <code>fnQualifiedName</code>.<code>props</code> call.
* example:
* isFunctionInvokeAndPropAccess(n, "getMode", "test"); // matches `getMode().test`
*/
private boolean isFunctionInvokeAndPropAccess(Node n, String fnQualifiedName, Set<String> props) {
// mode.getMode().localDev
// mode [property] ->
// getMode [call]
// ${property} [string]
if (!n.isGetProp()) {
return false;
}
Node call = n.getFirstChild();
if (!call.isCall()) {
return false;
}
Node fullQualifiedFnName = call.getFirstChild();
if (fullQualifiedFnName == null) {
return false;
}
String qualifiedName = fullQualifiedFnName.getQualifiedName();
if (qualifiedName != null && qualifiedName.endsWith(fnQualifiedName)) {
Node maybeProp = n.getSecondChild();
if (maybeProp != null && maybeProp.isString()) {
String name = maybeProp.getString();
for (String prop : props) {
if (prop == name) {
return true;
}
}
}
}
return false;
}
private void replaceWithBooleanExpression(boolean bool, Node n, Node parent) {
Node booleanNode = bool ? IR.trueNode() : IR.falseNode();
booleanNode.useSourceInfoIfMissingFrom(n);
parent.replaceChild(n, booleanNode);
compiler.reportCodeChange();
}
/**
* Checks if expression is a GETPROP() (method invocation) and the property
* name ends with one of the items in stripTypeSuffixes.
* This method does not do a deep check and will only do a shallow
* expression -> property -> call check.
*/
private boolean isNameStripType(Node n, Set<String> suffixes) {
if (!n.isCall()) {
return false;
}
Node getprop = n.getFirstChild();
if (getprop == null) {
return false;
}
return qualifiedNameEndsWithStripType(getprop, suffixes);
}
private void removeExpression(Node n, Node parent) {
if (parent.isExprResult()) {
Node grandparent = parent.getParent();
grandparent.removeChild(parent);
} else {
parent.removeChild(n);
}
compiler.reportCodeChange();
}
private void maybeEliminateCallExceptFirstParam(Node n, Node p) {
Node call = n.getFirstChild();
if (call == null) {
return;
}
Node firstArg = call.getNext();
if (firstArg == null) {
p.removeChild(n);
compiler.reportCodeChange();
return;
}
firstArg.detachFromParent();
p.replaceChild(n, firstArg);
compiler.reportCodeChange();
}
/**
* Checks the nodes qualified name if it ends with one of the items in
* stripTypeSuffixes
*/
boolean qualifiedNameEndsWithStripType(Node n, Set<String> suffixes) {
String name = n.getQualifiedName();
return qualifiedNameEndsWithStripType(name, suffixes);
}
/**
* Checks if the string ends with one of the items in stripTypeSuffixes
*/
boolean qualifiedNameEndsWithStripType(String name, Set<String> suffixes) {
if (name != null) {
for (String suffix : suffixes) {
if (name.endsWith(suffix)) {
return true;
}
}
}
return false;
}
}