ttsc 0.7.3 → 0.8.0-dev.20260505

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.
Files changed (33) hide show
  1. package/README.md +3 -4
  2. package/driver/rewrite.go +41 -0
  3. package/lib/compiler/internal/runBuild.js +47 -49
  4. package/lib/compiler/internal/runBuild.js.map +1 -1
  5. package/lib/compiler/internal/transformProjectInMemory.js +44 -4
  6. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  7. package/lib/launcher/internal/runTtsc.js +19 -1
  8. package/lib/launcher/internal/runTtsc.js.map +1 -1
  9. package/lib/plugin/internal/buildSourcePlugin.js +6 -0
  10. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  11. package/lib/plugin/internal/loadProjectPlugins.js +55 -2
  12. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  13. package/lib/structures/ITtscPlugin.d.ts +19 -3
  14. package/lib/structures/ITtscPlugin.js +4 -0
  15. package/lib/structures/ITtscPlugin.js.map +1 -1
  16. package/lib/structures/TtscPluginStage.d.ts +4 -10
  17. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
  18. package/package.json +11 -10
  19. package/shim/ast/lint.go +1 -0
  20. package/shim/ast/shim.go +1 -0
  21. package/shim/compiler/emit_hooks.go +494 -0
  22. package/shim/core/shim.go +3 -0
  23. package/src/compiler/internal/runBuild.ts +54 -85
  24. package/src/compiler/internal/transformProjectInMemory.ts +51 -6
  25. package/src/launcher/internal/runTtsc.ts +17 -1
  26. package/src/plugin/internal/buildSourcePlugin.ts +23 -0
  27. package/src/plugin/internal/loadProjectPlugins.ts +81 -2
  28. package/src/structures/ITtscPlugin.ts +30 -3
  29. package/src/structures/TtscPluginStage.ts +4 -10
  30. package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
  31. package/test/compiler.test.ts +3 -3
  32. package/test/tsgo.test.ts +9 -6
  33. package/utility/host.go +707 -0
@@ -0,0 +1,707 @@
1
+ package utility
2
+
3
+ import (
4
+ "encoding/json"
5
+ "flag"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "sort"
10
+ "strings"
11
+
12
+ shimast "github.com/microsoft/typescript-go/shim/ast"
13
+ shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
14
+ shimcore "github.com/microsoft/typescript-go/shim/core"
15
+
16
+ "github.com/samchon/ttsc/packages/ttsc/driver"
17
+ )
18
+
19
+ type pluginEntry struct {
20
+ Config map[string]any `json:"config"`
21
+ Hooks map[string]any `json:"hooks"`
22
+ Name string `json:"name"`
23
+ Stage string `json:"stage"`
24
+ }
25
+
26
+ type hostOptions struct {
27
+ cwd string
28
+ emit bool
29
+ noEmit bool
30
+ outDir string
31
+ pluginsJSON string
32
+ quiet bool
33
+ tsconfig string
34
+ verbose bool
35
+ }
36
+
37
+ type transformState struct {
38
+ banner string
39
+ paths *pathsRewriter
40
+ strip *stripRewriter
41
+ }
42
+
43
+ type transformResult struct {
44
+ Diagnostics []any `json:"diagnostics,omitempty"`
45
+ TypeScript map[string]string `json:"typescript"`
46
+ }
47
+
48
+ // RunBuild hosts first-party utility transform plugins inside one compiler emit.
49
+ func RunBuild(args []string) int {
50
+ opts, ok := parseHostOptions("build", args)
51
+ if !ok {
52
+ return 2
53
+ }
54
+ prog, entries, state, ok := loadUtilityProgram(opts)
55
+ if !ok {
56
+ return 2
57
+ }
58
+ defer prog.Close()
59
+ if opts.noEmit {
60
+ return 0
61
+ }
62
+ if opts.verbose {
63
+ opts.quiet = false
64
+ }
65
+ if !opts.quiet {
66
+ fmt.Fprintf(os.Stdout, "// ttsc utility: plugins=%d emit=%v\n", len(entries), !opts.noEmit)
67
+ }
68
+ var (
69
+ res *shimcompiler.EmitResult
70
+ eDiags []driver.Diagnostic
71
+ err error
72
+ )
73
+ if state.banner != "" {
74
+ hooks := shimcompiler.EmitHooks{
75
+ BeforePrintJS: func(context *shimcompiler.EmitContext, file *shimast.SourceFile) *shimast.SourceFile {
76
+ return applyBannerJSDoc(context, file, state.banner)
77
+ },
78
+ BeforePrintDeclaration: func(context *shimcompiler.EmitContext, file *shimast.SourceFile) *shimast.SourceFile {
79
+ return applyBannerJSDoc(context, file, state.banner)
80
+ },
81
+ }
82
+ res, eDiags, err = prog.EmitAllRawWithHooks(nil, hooks)
83
+ } else {
84
+ res, eDiags, err = prog.EmitAllRaw(nil)
85
+ }
86
+ if err != nil {
87
+ fmt.Fprintf(os.Stderr, "ttsc utility: emit failed: %v\n", err)
88
+ return 3
89
+ }
90
+ for _, d := range eDiags {
91
+ fmt.Fprintln(os.Stderr, " -", d.String())
92
+ }
93
+ if res != nil && !opts.quiet {
94
+ fmt.Fprintf(os.Stdout, "// ttsc utility: emitted=%d files\n", len(res.EmittedFiles))
95
+ }
96
+ return 0
97
+ }
98
+
99
+ // RunTransform returns the project TypeScript text after source-hook mutations.
100
+ func RunTransform(args []string) int {
101
+ opts, ok := parseHostOptions("transform", args)
102
+ if !ok {
103
+ return 2
104
+ }
105
+ prog, _, _, ok := loadUtilityProgram(opts)
106
+ if !ok {
107
+ return 2
108
+ }
109
+ defer prog.Close()
110
+ out := transformResult{TypeScript: map[string]string{}}
111
+ for _, file := range prog.SourceFiles() {
112
+ out.TypeScript[apiOutputKey(opts.cwd, file.FileName())] = file.Text()
113
+ }
114
+ data, err := json.Marshal(out)
115
+ if err != nil {
116
+ fmt.Fprintf(os.Stderr, "ttsc utility: transform marshal failed: %v\n", err)
117
+ return 3
118
+ }
119
+ fmt.Fprintln(os.Stdout, string(data))
120
+ return 0
121
+ }
122
+
123
+ func parseHostOptions(command string, args []string) (hostOptions, bool) {
124
+ fs := flag.NewFlagSet(command, flag.ContinueOnError)
125
+ fs.SetOutput(os.Stderr)
126
+ cwd := fs.String("cwd", "", "project directory")
127
+ emit := fs.Bool("emit", false, "force emit")
128
+ noEmit := fs.Bool("noEmit", false, "force no emit")
129
+ outDir := fs.String("outDir", "", "emit directory override")
130
+ pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
131
+ quiet := fs.Bool("quiet", true, "suppress summary")
132
+ tsconfig := fs.String("tsconfig", "tsconfig.json", "project tsconfig")
133
+ verbose := fs.Bool("verbose", false, "print summary")
134
+ if err := fs.Parse(args); err != nil {
135
+ return hostOptions{}, false
136
+ }
137
+ resolvedCwd := *cwd
138
+ if resolvedCwd == "" {
139
+ var err error
140
+ resolvedCwd, err = os.Getwd()
141
+ if err != nil {
142
+ fmt.Fprintf(os.Stderr, "ttsc utility: cwd: %v\n", err)
143
+ return hostOptions{}, false
144
+ }
145
+ }
146
+ if !filepath.IsAbs(resolvedCwd) {
147
+ abs, err := filepath.Abs(resolvedCwd)
148
+ if err != nil {
149
+ fmt.Fprintf(os.Stderr, "ttsc utility: cwd: %v\n", err)
150
+ return hostOptions{}, false
151
+ }
152
+ resolvedCwd = abs
153
+ }
154
+ return hostOptions{
155
+ cwd: filepath.Clean(resolvedCwd),
156
+ emit: *emit,
157
+ noEmit: *noEmit,
158
+ outDir: *outDir,
159
+ pluginsJSON: *pluginsJSON,
160
+ quiet: *quiet,
161
+ tsconfig: *tsconfig,
162
+ verbose: *verbose,
163
+ }, true
164
+ }
165
+
166
+ func loadUtilityProgram(opts hostOptions) (*driver.Program, []pluginEntry, transformState, bool) {
167
+ entries, err := parsePluginEntries(opts.pluginsJSON)
168
+ if err != nil {
169
+ fmt.Fprintln(os.Stderr, err)
170
+ return nil, nil, transformState{}, false
171
+ }
172
+ prog, diags, err := driver.LoadProgram(opts.cwd, opts.tsconfig, driver.LoadProgramOptions{
173
+ ForceEmit: opts.emit,
174
+ ForceNoEmit: opts.noEmit,
175
+ OutDir: opts.outDir,
176
+ })
177
+ if err != nil {
178
+ fmt.Fprintf(os.Stderr, "ttsc utility: %v\n", err)
179
+ return nil, nil, transformState{}, false
180
+ }
181
+ if len(diags) > 0 {
182
+ driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
183
+ return nil, nil, transformState{}, false
184
+ }
185
+ if diags := prog.Diagnostics(); len(diags) > 0 {
186
+ driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
187
+ return nil, nil, transformState{}, false
188
+ }
189
+ state, err := prepareTransforms(prog, entries)
190
+ if err != nil {
191
+ fmt.Fprintln(os.Stderr, err)
192
+ return nil, nil, transformState{}, false
193
+ }
194
+ if err := applySourceTransforms(prog, state); err != nil {
195
+ fmt.Fprintln(os.Stderr, err)
196
+ return nil, nil, transformState{}, false
197
+ }
198
+ return prog, entries, state, true
199
+ }
200
+
201
+ func parsePluginEntries(input string) ([]pluginEntry, error) {
202
+ if strings.TrimSpace(input) == "" {
203
+ return nil, nil
204
+ }
205
+ var entries []pluginEntry
206
+ if err := json.Unmarshal([]byte(input), &entries); err != nil {
207
+ return nil, fmt.Errorf("ttsc utility: invalid --plugins-json: %w", err)
208
+ }
209
+ return entries, nil
210
+ }
211
+
212
+ func prepareTransforms(prog *driver.Program, entries []pluginEntry) (transformState, error) {
213
+ state := transformState{}
214
+ for _, entry := range entries {
215
+ switch entry.Name {
216
+ case "@ttsc/banner":
217
+ banner, err := parseBanner(entry.Config)
218
+ if err != nil {
219
+ return state, err
220
+ }
221
+ state.banner = banner
222
+ case "@ttsc/paths":
223
+ state.paths = newPathsRewriter(prog)
224
+ case "@ttsc/strip":
225
+ strip, err := parseStrip(entry.Config)
226
+ if err != nil {
227
+ return state, err
228
+ }
229
+ state.strip = strip
230
+ }
231
+ }
232
+ return state, nil
233
+ }
234
+
235
+ func applySourceTransforms(prog *driver.Program, state transformState) error {
236
+ for _, file := range prog.SourceFiles() {
237
+ if state.paths != nil {
238
+ state.paths.apply(file)
239
+ }
240
+ if state.strip != nil {
241
+ state.strip.apply(file)
242
+ }
243
+ }
244
+ return nil
245
+ }
246
+
247
+ func apiOutputKey(cwd, fileName string) string {
248
+ rel, err := filepath.Rel(cwd, fileName)
249
+ if err != nil || strings.HasPrefix(rel, "..") {
250
+ return filepath.ToSlash(fileName)
251
+ }
252
+ return filepath.ToSlash(rel)
253
+ }
254
+
255
+ func parseBanner(config map[string]any) (string, error) {
256
+ raw, ok := config["banner"]
257
+ if !ok {
258
+ return "", fmt.Errorf("@ttsc/banner: \"banner\" must be a non-empty string")
259
+ }
260
+ text, ok := raw.(string)
261
+ if !ok || strings.TrimSpace(text) == "" {
262
+ return "", fmt.Errorf("@ttsc/banner: \"banner\" must be a non-empty string")
263
+ }
264
+ lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
265
+ for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
266
+ lines = lines[:len(lines)-1]
267
+ }
268
+ var b strings.Builder
269
+ sep := strings.Repeat("-", 64)
270
+ b.WriteString("*\n * ")
271
+ b.WriteString(sep)
272
+ b.WriteByte('\n')
273
+ for _, line := range lines {
274
+ b.WriteString(" * ")
275
+ b.WriteString(sanitizeJSDocLine(line))
276
+ b.WriteByte('\n')
277
+ }
278
+ b.WriteString(" *\n")
279
+ b.WriteString(" * @packageDocumentation\n ")
280
+ return b.String(), nil
281
+ }
282
+
283
+ func sanitizeJSDocLine(line string) string {
284
+ return strings.ReplaceAll(line, "*/", "* /")
285
+ }
286
+
287
+ func applyBannerJSDoc(context *shimcompiler.EmitContext, file *shimast.SourceFile, banner string) *shimast.SourceFile {
288
+ if context == nil || file == nil || banner == "" || file.Statements == nil || len(file.Statements.Nodes) == 0 {
289
+ return file
290
+ }
291
+ context.AddSyntheticLeadingComment(file.Statements.Nodes[0], shimast.KindMultiLineCommentTrivia, banner, true)
292
+ return file
293
+ }
294
+
295
+ type pathsRewriter struct {
296
+ basePath string
297
+ outDir string
298
+ patterns []pathsPattern
299
+ rootDir string
300
+ sourceFiles map[string]string
301
+ }
302
+
303
+ type pathsPattern struct {
304
+ pattern string
305
+ targets []string
306
+ }
307
+
308
+ func newPathsRewriter(prog *driver.Program) *pathsRewriter {
309
+ out := &pathsRewriter{sourceFiles: map[string]string{}}
310
+ if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
311
+ return out
312
+ }
313
+ options := prog.ParsedConfig.ParsedConfig.CompilerOptions
314
+ out.basePath = filepath.Clean(options.GetPathsBasePath(prog.Host.GetCurrentDirectory()))
315
+ out.outDir = optionalPath(options.OutDir, prog.Host.GetCurrentDirectory())
316
+ out.rootDir = optionalPath(options.RootDir, prog.Host.GetCurrentDirectory())
317
+ files := prog.SourceFiles()
318
+ if out.rootDir == "" {
319
+ out.rootDir = commonSourceDir(files)
320
+ }
321
+ for _, file := range files {
322
+ name := normalizePath(file.FileName())
323
+ out.sourceFiles[name] = name
324
+ out.sourceFiles[stripKnownSourceExtension(name)] = name
325
+ }
326
+ if options.Paths != nil {
327
+ for pattern, targets := range options.Paths.Entries() {
328
+ out.patterns = append(out.patterns, pathsPattern{
329
+ pattern: pattern,
330
+ targets: append([]string(nil), targets...),
331
+ })
332
+ }
333
+ }
334
+ sort.SliceStable(out.patterns, func(i, j int) bool {
335
+ return patternRank(out.patterns[i].pattern) > patternRank(out.patterns[j].pattern)
336
+ })
337
+ return out
338
+ }
339
+
340
+ func (r *pathsRewriter) apply(file *shimast.SourceFile) {
341
+ if r == nil || file == nil || len(r.patterns) == 0 {
342
+ return
343
+ }
344
+ visitModuleSpecifiers(file.AsNode(), func(lit *shimast.Node) {
345
+ if lit == nil || lit.Kind != shimast.KindStringLiteral {
346
+ return
347
+ }
348
+ spec := lit.Text()
349
+ rewritten, ok := r.rewrite(file.FileName(), spec)
350
+ if ok && rewritten != spec {
351
+ lit.AsStringLiteral().Text = rewritten
352
+ lit.Flags |= shimast.NodeFlagsSynthesized
353
+ lit.Loc = shimcore.UndefinedTextRange()
354
+ }
355
+ })
356
+ }
357
+
358
+ func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
359
+ if node == nil {
360
+ return
361
+ }
362
+ switch node.Kind {
363
+ case shimast.KindImportDeclaration:
364
+ visit(node.AsImportDeclaration().ModuleSpecifier)
365
+ case shimast.KindExportDeclaration:
366
+ visit(node.AsExportDeclaration().ModuleSpecifier)
367
+ case shimast.KindImportEqualsDeclaration:
368
+ ref := node.AsImportEqualsDeclaration().ModuleReference
369
+ if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
370
+ visit(ref.AsExternalModuleReference().Expression)
371
+ }
372
+ case shimast.KindImportType:
373
+ arg := node.AsImportTypeNode().Argument
374
+ if arg != nil && arg.Kind == shimast.KindLiteralType {
375
+ visit(arg.AsLiteralTypeNode().Literal)
376
+ }
377
+ case shimast.KindCallExpression:
378
+ call := node.AsCallExpression()
379
+ if call.Expression != nil && call.Expression.Kind == shimast.KindImportKeyword && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
380
+ visit(call.Arguments.Nodes[0])
381
+ }
382
+ }
383
+ node.ForEachChild(func(child *shimast.Node) bool {
384
+ visitModuleSpecifiers(child, visit)
385
+ return false
386
+ })
387
+ }
388
+
389
+ func (r *pathsRewriter) rewrite(fromSource string, specifier string) (string, bool) {
390
+ if specifier == "" || strings.HasPrefix(specifier, ".") || strings.HasPrefix(specifier, "/") {
391
+ return specifier, false
392
+ }
393
+ targetSource, ok := r.resolveSource(specifier)
394
+ if !ok {
395
+ return specifier, false
396
+ }
397
+ fromOut := r.outputPathForSource(fromSource)
398
+ targetOut := r.outputPathForSource(targetSource)
399
+ if fromOut == "" || targetOut == "" {
400
+ return specifier, false
401
+ }
402
+ rel, err := filepath.Rel(filepath.Dir(fromOut), targetOut)
403
+ if err != nil {
404
+ return specifier, false
405
+ }
406
+ rel = filepath.ToSlash(rel)
407
+ if !strings.HasPrefix(rel, ".") {
408
+ rel = "./" + rel
409
+ }
410
+ return rel, true
411
+ }
412
+
413
+ func (r *pathsRewriter) resolveSource(specifier string) (string, bool) {
414
+ for _, pattern := range r.patterns {
415
+ star, ok := matchPattern(pattern.pattern, specifier)
416
+ if !ok {
417
+ continue
418
+ }
419
+ for _, target := range pattern.targets {
420
+ candidate := strings.Replace(target, "*", star, 1)
421
+ resolved := normalizePath(filepath.Join(r.basePath, candidate))
422
+ if source, ok := r.lookupSource(resolved); ok {
423
+ return source, true
424
+ }
425
+ }
426
+ }
427
+ return "", false
428
+ }
429
+
430
+ func (r *pathsRewriter) lookupSource(candidate string) (string, bool) {
431
+ if source, ok := r.sourceFiles[normalizePath(candidate)]; ok {
432
+ return source, true
433
+ }
434
+ stem := stripKnownSourceExtension(normalizePath(candidate))
435
+ if source, ok := r.sourceFiles[stem]; ok {
436
+ return source, true
437
+ }
438
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
439
+ if source, ok := r.sourceFiles[stem+ext]; ok {
440
+ return source, true
441
+ }
442
+ }
443
+ return "", false
444
+ }
445
+
446
+ func (r *pathsRewriter) outputPathForSource(source string) string {
447
+ if r.outDir == "" || r.rootDir == "" {
448
+ return ""
449
+ }
450
+ rel, err := filepath.Rel(r.rootDir, source)
451
+ if err != nil || strings.HasPrefix(rel, "..") {
452
+ return ""
453
+ }
454
+ return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, ".js")))
455
+ }
456
+
457
+ func matchPattern(pattern string, specifier string) (string, bool) {
458
+ if !strings.Contains(pattern, "*") {
459
+ return "", pattern == specifier
460
+ }
461
+ parts := strings.SplitN(pattern, "*", 2)
462
+ if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) {
463
+ return "", false
464
+ }
465
+ return specifier[len(parts[0]) : len(specifier)-len(parts[1])], true
466
+ }
467
+
468
+ func patternRank(pattern string) int {
469
+ return len(strings.ReplaceAll(pattern, "*", ""))
470
+ }
471
+
472
+ func optionalPath(value string, cwd string) string {
473
+ if value == "" {
474
+ return ""
475
+ }
476
+ if filepath.IsAbs(value) {
477
+ return normalizePath(value)
478
+ }
479
+ return normalizePath(filepath.Join(cwd, value))
480
+ }
481
+
482
+ func commonSourceDir(files []*shimast.SourceFile) string {
483
+ if len(files) == 0 {
484
+ return ""
485
+ }
486
+ common := normalizePath(filepath.Dir(files[0].FileName()))
487
+ for _, file := range files[1:] {
488
+ dir := normalizePath(filepath.Dir(file.FileName()))
489
+ for common != "" && !strings.HasPrefix(dir+"/", common+"/") {
490
+ next := filepath.Dir(common)
491
+ if next == common {
492
+ return common
493
+ }
494
+ common = normalizePath(next)
495
+ }
496
+ }
497
+ return common
498
+ }
499
+
500
+ func normalizePath(value string) string {
501
+ if value == "" {
502
+ return ""
503
+ }
504
+ return filepath.ToSlash(filepath.Clean(value))
505
+ }
506
+
507
+ func stripKnownSourceExtension(value string) string {
508
+ lower := strings.ToLower(value)
509
+ for _, ext := range []string{".d.ts", ".d.mts", ".d.cts", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
510
+ if strings.HasSuffix(lower, ext) {
511
+ return value[:len(value)-len(ext)]
512
+ }
513
+ }
514
+ return strings.TrimSuffix(value, filepath.Ext(value))
515
+ }
516
+
517
+ func replaceSourceExtension(value string, ext string) string {
518
+ return stripKnownSourceExtension(filepath.ToSlash(value)) + ext
519
+ }
520
+
521
+ type stripRewriter struct {
522
+ calls []callPattern
523
+ stripDebugger bool
524
+ }
525
+
526
+ type callPattern struct {
527
+ parts []string
528
+ wildcard bool
529
+ }
530
+
531
+ func parseStrip(config map[string]any) (*stripRewriter, error) {
532
+ calls, err := stringArrayConfig(config, "calls")
533
+ if err != nil {
534
+ return nil, fmt.Errorf("@ttsc/strip: %w", err)
535
+ }
536
+ statements, err := stringArrayConfig(config, "statements")
537
+ if err != nil {
538
+ return nil, fmt.Errorf("@ttsc/strip: %w", err)
539
+ }
540
+ out := &stripRewriter{}
541
+ for _, call := range calls {
542
+ pattern, err := parseCallPattern(call)
543
+ if err != nil {
544
+ return nil, fmt.Errorf("@ttsc/strip: %w", err)
545
+ }
546
+ out.calls = append(out.calls, pattern)
547
+ }
548
+ for _, statement := range statements {
549
+ switch statement {
550
+ case "debugger":
551
+ out.stripDebugger = true
552
+ default:
553
+ return nil, fmt.Errorf("@ttsc/strip: unsupported statement pattern %q", statement)
554
+ }
555
+ }
556
+ return out, nil
557
+ }
558
+
559
+ func (s *stripRewriter) apply(file *shimast.SourceFile) {
560
+ if s == nil || file == nil || (len(s.calls) == 0 && !s.stripDebugger) {
561
+ return
562
+ }
563
+ filterStatements(file.Statements, s)
564
+ }
565
+
566
+ func filterStatements(list *shimast.NodeList, strip *stripRewriter) {
567
+ if list == nil || len(list.Nodes) == 0 {
568
+ return
569
+ }
570
+ out := list.Nodes[:0]
571
+ for _, stmt := range list.Nodes {
572
+ if shouldStripStatement(stmt, strip) {
573
+ continue
574
+ }
575
+ filterChildStatements(stmt, strip)
576
+ out = append(out, stmt)
577
+ }
578
+ list.Nodes = out
579
+ }
580
+
581
+ func filterChildStatements(node *shimast.Node, strip *stripRewriter) {
582
+ if node == nil {
583
+ return
584
+ }
585
+ if node.CanHaveStatements() {
586
+ filterStatements(node.StatementList(), strip)
587
+ }
588
+ node.ForEachChild(func(child *shimast.Node) bool {
589
+ filterChildStatements(child, strip)
590
+ return false
591
+ })
592
+ }
593
+
594
+ func shouldStripStatement(node *shimast.Node, strip *stripRewriter) bool {
595
+ if node == nil {
596
+ return false
597
+ }
598
+ switch node.Kind {
599
+ case shimast.KindDebuggerStatement:
600
+ return strip.stripDebugger
601
+ case shimast.KindExpressionStatement:
602
+ expr := node.AsExpressionStatement().Expression
603
+ name, ok := callExpressionName(expr)
604
+ return ok && strip.matchesCall(name)
605
+ default:
606
+ return false
607
+ }
608
+ }
609
+
610
+ func (s *stripRewriter) matchesCall(name string) bool {
611
+ for _, pattern := range s.calls {
612
+ if pattern.matches(name) {
613
+ return true
614
+ }
615
+ }
616
+ return false
617
+ }
618
+
619
+ func parseCallPattern(text string) (callPattern, error) {
620
+ parts := strings.Split(text, ".")
621
+ if len(parts) == 0 {
622
+ return callPattern{}, fmt.Errorf("empty call pattern")
623
+ }
624
+ for i, part := range parts {
625
+ if part == "" {
626
+ return callPattern{}, fmt.Errorf("invalid call pattern %q", text)
627
+ }
628
+ if part == "*" && i != len(parts)-1 {
629
+ return callPattern{}, fmt.Errorf("wildcard is only supported at the end of call pattern %q", text)
630
+ }
631
+ }
632
+ wildcard := parts[len(parts)-1] == "*"
633
+ if wildcard {
634
+ parts = parts[:len(parts)-1]
635
+ }
636
+ return callPattern{parts: parts, wildcard: wildcard}, nil
637
+ }
638
+
639
+ func (p callPattern) matches(name string) bool {
640
+ parts := strings.Split(name, ".")
641
+ if p.wildcard {
642
+ if len(parts) <= len(p.parts) {
643
+ return false
644
+ }
645
+ return equalStringSlices(parts[:len(p.parts)], p.parts)
646
+ }
647
+ return equalStringSlices(parts, p.parts)
648
+ }
649
+
650
+ func callExpressionName(expr *shimast.Node) (string, bool) {
651
+ if expr == nil || expr.Kind != shimast.KindCallExpression {
652
+ return "", false
653
+ }
654
+ call := expr.AsCallExpression()
655
+ return dottedName(call.Expression)
656
+ }
657
+
658
+ func dottedName(expr *shimast.Node) (string, bool) {
659
+ if expr == nil {
660
+ return "", false
661
+ }
662
+ switch expr.Kind {
663
+ case shimast.KindIdentifier:
664
+ return expr.Text(), true
665
+ case shimast.KindPropertyAccessExpression:
666
+ prop := expr.AsPropertyAccessExpression()
667
+ left, ok := dottedName(prop.Expression)
668
+ if !ok || prop.Name() == nil {
669
+ return "", false
670
+ }
671
+ return left + "." + prop.Name().Text(), true
672
+ default:
673
+ return "", false
674
+ }
675
+ }
676
+
677
+ func stringArrayConfig(config map[string]any, key string) ([]string, error) {
678
+ raw, ok := config[key]
679
+ if !ok || raw == nil {
680
+ return nil, nil
681
+ }
682
+ values, ok := raw.([]any)
683
+ if !ok {
684
+ return nil, fmt.Errorf("%q must be an array of strings", key)
685
+ }
686
+ out := make([]string, 0, len(values))
687
+ for i, value := range values {
688
+ text, ok := value.(string)
689
+ if !ok || strings.TrimSpace(text) == "" {
690
+ return nil, fmt.Errorf("%q[%d] must be a non-empty string", key, i)
691
+ }
692
+ out = append(out, text)
693
+ }
694
+ return out, nil
695
+ }
696
+
697
+ func equalStringSlices(left, right []string) bool {
698
+ if len(left) != len(right) {
699
+ return false
700
+ }
701
+ for i := range left {
702
+ if left[i] != right[i] {
703
+ return false
704
+ }
705
+ }
706
+ return true
707
+ }