@ttsc/strip 0.12.3 → 0.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/driver/strip.go +245 -203
- package/package.json +1 -1
package/driver/strip.go
CHANGED
|
@@ -1,268 +1,310 @@
|
|
|
1
1
|
package strip
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
func init() {
|
|
13
|
-
|
|
13
|
+
driver.RegisterPlugin(plugin{})
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// plugin implements driver.ProgramPlugin for @ttsc/strip.
|
|
16
17
|
type plugin struct{}
|
|
17
18
|
|
|
19
|
+
// ApplyProgram strips configured call expressions and debugger statements from
|
|
20
|
+
// every source file in the program.
|
|
18
21
|
func (plugin) ApplyProgram(prog *driver.Program, ctx driver.PluginContext) error {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
rewriter, err := parseStrip(ctx.Entry.Config)
|
|
23
|
+
if err != nil {
|
|
24
|
+
return err
|
|
25
|
+
}
|
|
26
|
+
for _, file := range prog.SourceFiles() {
|
|
27
|
+
rewriter.apply(file)
|
|
28
|
+
}
|
|
29
|
+
return nil
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
// stripRewriter holds the resolved strip configuration for a single build.
|
|
29
33
|
type stripRewriter struct {
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
calls []callPattern
|
|
35
|
+
stripDebugger bool
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
// callPattern represents a parsed call-expression stripping rule such as
|
|
39
|
+
// "console.log" (exact) or "assert.*" (wildcard prefix).
|
|
34
40
|
type callPattern struct {
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
parts []string
|
|
42
|
+
wildcard bool
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
// parseStrip builds a stripRewriter from the plugin config map. When neither
|
|
46
|
+
// "calls" nor "statements" is present the default configuration is applied:
|
|
47
|
+
// strip console.log, console.debug, assert.*, and debugger statements.
|
|
39
48
|
func parseStrip(config map[string]any) (*stripRewriter, error) {
|
|
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
|
-
|
|
49
|
+
_, hasCalls := config["calls"]
|
|
50
|
+
_, hasStatements := config["statements"]
|
|
51
|
+
if !hasCalls && !hasStatements {
|
|
52
|
+
config = map[string]any{
|
|
53
|
+
"calls": []any{"console.log", "console.debug", "assert.*"},
|
|
54
|
+
"statements": []any{"debugger"},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
calls, err := stringArrayConfig(config, "calls")
|
|
58
|
+
if err != nil {
|
|
59
|
+
return nil, fmt.Errorf("@ttsc/strip: %w", err)
|
|
60
|
+
}
|
|
61
|
+
statements, err := stringArrayConfig(config, "statements")
|
|
62
|
+
if err != nil {
|
|
63
|
+
return nil, fmt.Errorf("@ttsc/strip: %w", err)
|
|
64
|
+
}
|
|
65
|
+
out := &stripRewriter{}
|
|
66
|
+
for _, call := range calls {
|
|
67
|
+
pattern, err := parseCallPattern(call)
|
|
68
|
+
if err != nil {
|
|
69
|
+
return nil, fmt.Errorf("@ttsc/strip: %w", err)
|
|
70
|
+
}
|
|
71
|
+
out.calls = append(out.calls, pattern)
|
|
72
|
+
}
|
|
73
|
+
for _, statement := range statements {
|
|
74
|
+
switch statement {
|
|
75
|
+
case "debugger":
|
|
76
|
+
out.stripDebugger = true
|
|
77
|
+
default:
|
|
78
|
+
return nil, fmt.Errorf("@ttsc/strip: unsupported statement pattern %q", statement)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out, nil
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
// apply removes matching statements from file's top-level statement list.
|
|
75
85
|
func (s *stripRewriter) apply(file *shimast.SourceFile) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
if s == nil || file == nil || (len(s.calls) == 0 && !s.stripDebugger) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
filterStatements(file.Statements, s)
|
|
80
90
|
}
|
|
81
91
|
|
|
92
|
+
// filterStatements removes stripped statements from list in-place, preserving
|
|
93
|
+
// order. Children of retained statements are recursively filtered.
|
|
82
94
|
func filterStatements(list *shimast.NodeList, strip *stripRewriter) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
if list == nil || len(list.Nodes) == 0 {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
out := make([]*shimast.Node, 0, len(list.Nodes))
|
|
99
|
+
for _, stmt := range list.Nodes {
|
|
100
|
+
if shouldStripStatement(stmt, strip) {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
filterChildStatements(stmt, strip)
|
|
104
|
+
out = append(out, stmt)
|
|
105
|
+
}
|
|
106
|
+
list.Nodes = out
|
|
95
107
|
}
|
|
96
108
|
|
|
109
|
+
// filterChildStatements recurses into node's children, filtering embedded
|
|
110
|
+
// single-statement bodies (if, while, for, etc.) and nested statement lists.
|
|
97
111
|
func filterChildStatements(node *shimast.Node, strip *stripRewriter) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
if node == nil {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
filterEmbeddedStatements(node, strip)
|
|
116
|
+
if node.CanHaveStatements() {
|
|
117
|
+
filterStatements(node.StatementList(), strip)
|
|
118
|
+
}
|
|
119
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
120
|
+
filterChildStatements(child, strip)
|
|
121
|
+
return false
|
|
122
|
+
})
|
|
109
123
|
}
|
|
110
124
|
|
|
125
|
+
// filterEmbeddedStatements handles statement nodes that embed a single child
|
|
126
|
+
// statement (if/else, do, while, for, with, labeled). A stripped child is
|
|
127
|
+
// replaced with an empty synthesized statement to preserve the AST shape.
|
|
111
128
|
func filterEmbeddedStatements(node *shimast.Node, strip *stripRewriter) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
switch node.Kind {
|
|
130
|
+
case shimast.KindIfStatement:
|
|
131
|
+
stmt := node.AsIfStatement()
|
|
132
|
+
stmt.ThenStatement = filterEmbeddedStatement(stmt.ThenStatement, strip)
|
|
133
|
+
stmt.ElseStatement = filterEmbeddedStatement(stmt.ElseStatement, strip)
|
|
134
|
+
case shimast.KindDoStatement:
|
|
135
|
+
stmt := node.AsDoStatement()
|
|
136
|
+
stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
|
|
137
|
+
case shimast.KindWhileStatement:
|
|
138
|
+
stmt := node.AsWhileStatement()
|
|
139
|
+
stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
|
|
140
|
+
case shimast.KindForStatement:
|
|
141
|
+
stmt := node.AsForStatement()
|
|
142
|
+
stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
|
|
143
|
+
case shimast.KindForInStatement, shimast.KindForOfStatement:
|
|
144
|
+
stmt := node.AsForInOrOfStatement()
|
|
145
|
+
stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
|
|
146
|
+
case shimast.KindWithStatement:
|
|
147
|
+
stmt := node.AsWithStatement()
|
|
148
|
+
stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
|
|
149
|
+
case shimast.KindLabeledStatement:
|
|
150
|
+
stmt := node.AsLabeledStatement()
|
|
151
|
+
stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
|
|
152
|
+
}
|
|
136
153
|
}
|
|
137
154
|
|
|
155
|
+
// filterEmbeddedStatement strips or recurses into a single embedded statement.
|
|
156
|
+
// Returns an empty synthesized statement when stmt is to be stripped, preserving
|
|
157
|
+
// the original source location for downstream source-map accuracy.
|
|
138
158
|
func filterEmbeddedStatement(stmt *shimast.Statement, strip *stripRewriter) *shimast.Statement {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
159
|
+
if stmt == nil {
|
|
160
|
+
return nil
|
|
161
|
+
}
|
|
162
|
+
if shouldStripStatement(stmt, strip) {
|
|
163
|
+
return emptyStatement(stmt)
|
|
164
|
+
}
|
|
165
|
+
filterChildStatements(stmt, strip)
|
|
166
|
+
return stmt
|
|
147
167
|
}
|
|
148
168
|
|
|
169
|
+
// emptyStatement creates a synthesized empty statement (";") that inherits
|
|
170
|
+
// original's source location, used as a no-op placeholder after stripping.
|
|
149
171
|
func emptyStatement(original *shimast.Node) *shimast.Statement {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
172
|
+
empty := shimast.NewNodeFactory(shimast.NodeFactoryHooks{}).NewEmptyStatement()
|
|
173
|
+
empty.Flags |= shimast.NodeFlagsSynthesized
|
|
174
|
+
if original != nil {
|
|
175
|
+
empty.Loc = original.Loc
|
|
176
|
+
}
|
|
177
|
+
return empty
|
|
156
178
|
}
|
|
157
179
|
|
|
180
|
+
// shouldStripStatement reports whether node should be removed based on the
|
|
181
|
+
// current strip configuration. Only debugger and expression statements are
|
|
182
|
+
// candidates; all other statement kinds are retained.
|
|
158
183
|
func shouldStripStatement(node *shimast.Node, strip *stripRewriter) bool {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
if node == nil {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
switch node.Kind {
|
|
188
|
+
case shimast.KindDebuggerStatement:
|
|
189
|
+
return strip.stripDebugger
|
|
190
|
+
case shimast.KindExpressionStatement:
|
|
191
|
+
expr := node.AsExpressionStatement().Expression
|
|
192
|
+
name, ok := callExpressionName(expr)
|
|
193
|
+
return ok && strip.matchesCall(name)
|
|
194
|
+
default:
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
172
197
|
}
|
|
173
198
|
|
|
199
|
+
// matchesCall reports whether name matches any of the configured call patterns.
|
|
174
200
|
func (s *stripRewriter) matchesCall(name string) bool {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
201
|
+
for _, pattern := range s.calls {
|
|
202
|
+
if pattern.matches(name) {
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return false
|
|
181
207
|
}
|
|
182
208
|
|
|
209
|
+
// parseCallPattern parses a dot-separated call pattern string such as
|
|
210
|
+
// "console.log" or "assert.*". A wildcard ("*") is only allowed as the
|
|
211
|
+
// final segment. Empty segments are rejected.
|
|
183
212
|
func parseCallPattern(text string) (callPattern, error) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
213
|
+
parts := strings.Split(text, ".")
|
|
214
|
+
for i, part := range parts {
|
|
215
|
+
if part == "" {
|
|
216
|
+
return callPattern{}, fmt.Errorf("invalid call pattern %q", text)
|
|
217
|
+
}
|
|
218
|
+
if part == "*" && i != len(parts)-1 {
|
|
219
|
+
return callPattern{}, fmt.Errorf("wildcard is only supported at the end of call pattern %q", text)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
wildcard := parts[len(parts)-1] == "*"
|
|
223
|
+
if wildcard {
|
|
224
|
+
parts = parts[:len(parts)-1]
|
|
225
|
+
}
|
|
226
|
+
return callPattern{parts: parts, wildcard: wildcard}, nil
|
|
198
227
|
}
|
|
199
228
|
|
|
229
|
+
// matches reports whether a dotted call name (e.g. "console.log") matches
|
|
230
|
+
// the pattern. Wildcard patterns require at least one extra segment beyond
|
|
231
|
+
// the pattern prefix.
|
|
200
232
|
func (p callPattern) matches(name string) bool {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
233
|
+
parts := strings.Split(name, ".")
|
|
234
|
+
if p.wildcard {
|
|
235
|
+
if len(parts) <= len(p.parts) {
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
return equalStringSlices(parts[:len(p.parts)], p.parts)
|
|
239
|
+
}
|
|
240
|
+
return equalStringSlices(parts, p.parts)
|
|
209
241
|
}
|
|
210
242
|
|
|
243
|
+
// callExpressionName extracts the dotted callee name from a call expression
|
|
244
|
+
// node, e.g. "console.log" from `console.log(...)`. Returns ("", false) when
|
|
245
|
+
// expr is not a call expression or the callee is not a dotted identifier chain.
|
|
211
246
|
func callExpressionName(expr *shimast.Node) (string, bool) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
247
|
+
if expr == nil || expr.Kind != shimast.KindCallExpression {
|
|
248
|
+
return "", false
|
|
249
|
+
}
|
|
250
|
+
call := expr.AsCallExpression()
|
|
251
|
+
return dottedName(call.Expression)
|
|
217
252
|
}
|
|
218
253
|
|
|
254
|
+
// dottedName recursively extracts a dot-joined identifier chain from an
|
|
255
|
+
// expression node. Returns ("", false) for any non-identifier, non-property-
|
|
256
|
+
// access node.
|
|
219
257
|
func dottedName(expr *shimast.Node) (string, bool) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
258
|
+
if expr == nil {
|
|
259
|
+
return "", false
|
|
260
|
+
}
|
|
261
|
+
switch expr.Kind {
|
|
262
|
+
case shimast.KindIdentifier:
|
|
263
|
+
return expr.Text(), true
|
|
264
|
+
case shimast.KindPropertyAccessExpression:
|
|
265
|
+
prop := expr.AsPropertyAccessExpression()
|
|
266
|
+
left, ok := dottedName(prop.Expression)
|
|
267
|
+
if !ok || prop.Name() == nil {
|
|
268
|
+
return "", false
|
|
269
|
+
}
|
|
270
|
+
return left + "." + prop.Name().Text(), true
|
|
271
|
+
default:
|
|
272
|
+
return "", false
|
|
273
|
+
}
|
|
236
274
|
}
|
|
237
275
|
|
|
276
|
+
// stringArrayConfig reads a string array from config[key]. Returns nil when the
|
|
277
|
+
// key is absent. Returns an error when the value is not an array of non-empty strings.
|
|
238
278
|
func stringArrayConfig(config map[string]any, key string) ([]string, error) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
279
|
+
raw, ok := config[key]
|
|
280
|
+
if !ok || raw == nil {
|
|
281
|
+
return nil, nil
|
|
282
|
+
}
|
|
283
|
+
values, ok := raw.([]any)
|
|
284
|
+
if !ok {
|
|
285
|
+
return nil, fmt.Errorf("%q must be an array of strings", key)
|
|
286
|
+
}
|
|
287
|
+
out := make([]string, 0, len(values))
|
|
288
|
+
for i, value := range values {
|
|
289
|
+
text, ok := value.(string)
|
|
290
|
+
if !ok || strings.TrimSpace(text) == "" {
|
|
291
|
+
return nil, fmt.Errorf("%q[%d] must be a non-empty string", key, i)
|
|
292
|
+
}
|
|
293
|
+
out = append(out, text)
|
|
294
|
+
}
|
|
295
|
+
return out, nil
|
|
256
296
|
}
|
|
257
297
|
|
|
298
|
+
// equalStringSlices reports whether left and right contain the same strings in
|
|
299
|
+
// the same order.
|
|
258
300
|
func equalStringSlices(left, right []string) bool {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
301
|
+
if len(left) != len(right) {
|
|
302
|
+
return false
|
|
303
|
+
}
|
|
304
|
+
for i := range left {
|
|
305
|
+
if left[i] != right[i] {
|
|
306
|
+
return false
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return true
|
|
268
310
|
}
|