ttsc 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/cmd/platform/main.go +9 -0
- package/cmd/ttsc/api_compile.go +109 -101
- package/cmd/ttsc/api_compile_branches_test.go +55 -57
- package/cmd/ttsc/api_transform.go +54 -49
- package/cmd/ttsc/api_transform_branches_test.go +30 -32
- package/cmd/ttsc/build.go +111 -108
- package/cmd/ttsc/build_branches_test.go +95 -97
- package/cmd/ttsc/helpers_test.go +39 -28
- package/cmd/ttsc/main.go +111 -104
- package/cmd/ttsc/main_alias_branches_test.go +21 -23
- package/cmd/ttscserver/main.go +96 -90
- package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +22 -22
- package/cmd/ttscserver/run_lsp_reports_server_error_test.go +22 -22
- package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +23 -23
- package/cmd/utility-host/main.go +30 -23
- package/driver/lsp.go +53 -7
- package/driver/plugins.go +88 -72
- package/driver/program.go +362 -339
- package/driver/rewrite.go +442 -380
- package/internal/lspserver/lsp_envelope.go +64 -64
- package/internal/lspserver/lsp_frame.go +60 -60
- package/internal/lspserver/lsp_plugin.go +57 -57
- package/internal/lspserver/lsp_proxy.go +265 -251
- package/internal/lspserver/lsp_server.go +155 -144
- package/lib/TtscCompiler.d.ts +7 -0
- package/lib/TtscCompiler.js +7 -0
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/buildNativeCompiler.d.ts +11 -1
- package/lib/compiler/internal/buildNativeCompiler.js +23 -3
- package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.d.ts +9 -0
- package/lib/compiler/internal/compileProjectInMemory.js +36 -21
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/paths.d.ts +27 -0
- package/lib/compiler/internal/paths.js +70 -0
- package/lib/compiler/internal/paths.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +9 -1
- package/lib/compiler/internal/project/readProjectConfig.js +80 -1
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/project/resolveProjectConfig.d.ts +13 -1
- package/lib/compiler/internal/project/resolveProjectConfig.js +24 -1
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/resolveBinary.d.ts +9 -1
- package/lib/compiler/internal/resolveBinary.js +17 -1
- package/lib/compiler/internal/resolveBinary.js.map +1 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +16 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +42 -10
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/resolveTsgo.d.ts +21 -1
- package/lib/compiler/internal/resolveTsgo.js +23 -1
- package/lib/compiler/internal/resolveTsgo.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +19 -0
- package/lib/compiler/internal/runBuild.js +112 -46
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.d.ts +10 -1
- package/lib/compiler/internal/runSingleFileEmit.js +14 -1
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +5 -0
- package/lib/compiler/internal/sharedHostHelpers.js +6 -2
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/spawnNative.d.ts +21 -0
- package/lib/compiler/internal/spawnNative.js +59 -0
- package/lib/compiler/internal/spawnNative.js.map +1 -0
- package/lib/compiler/internal/transformProjectInMemory.d.ts +15 -1
- package/lib/compiler/internal/transformProjectInMemory.js +57 -61
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/launcher/internal/getCompilerVersionText.js +7 -0
- package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +32 -0
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/resolveCacheDir.d.ts +9 -0
- package/lib/launcher/internal/resolveCacheDir.js +22 -0
- package/lib/launcher/internal/resolveCacheDir.js.map +1 -0
- package/lib/launcher/internal/resolveTtscserverBinary.js +2 -0
- package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -1
- package/lib/launcher/internal/runTtsc.d.ts +10 -0
- package/lib/launcher/internal/runTtsc.js +22 -9
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +10 -35
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.d.ts +8 -0
- package/lib/launcher/internal/runTtsx.js +33 -7
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +38 -0
- package/lib/plugin/internal/buildSourcePlugin.js +40 -17
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +26 -0
- package/lib/plugin/internal/loadProjectPlugins.js +32 -18
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +15 -2
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +0 -2
- package/package.json +9 -9
- package/shim/ast/shim.go +24 -0
- package/shim/checker/shim.go +35 -0
- package/shim/core/shim.go +17 -0
- package/shim/diagnosticwriter/shim.go +4 -0
- package/shim/lsp/shim.go +3 -3
- package/shim/printer/shim.go +20 -0
- package/src/TtscCompiler.ts +7 -0
- package/src/compiler/internal/buildNativeCompiler.ts +23 -3
- package/src/compiler/internal/compileProjectInMemory.ts +34 -25
- package/src/compiler/internal/paths.ts +68 -0
- package/src/compiler/internal/project/readProjectConfig.ts +86 -1
- package/src/compiler/internal/project/resolveProjectConfig.ts +24 -1
- package/src/compiler/internal/resolveBinary.ts +17 -1
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -12
- package/src/compiler/internal/resolveTsgo.ts +29 -1
- package/src/compiler/internal/runBuild.ts +110 -55
- package/src/compiler/internal/runSingleFileEmit.ts +14 -1
- package/src/compiler/internal/sharedHostHelpers.ts +7 -7
- package/src/compiler/internal/spawnNative.ts +64 -0
- package/src/compiler/internal/transformProjectInMemory.ts +47 -67
- package/src/launcher/internal/getCompilerVersionText.ts +7 -0
- package/src/launcher/internal/prepareExecution.ts +32 -0
- package/src/launcher/internal/resolveCacheDir.ts +19 -0
- package/src/launcher/internal/resolveTtscserverBinary.ts +2 -0
- package/src/launcher/internal/runTtsc.ts +22 -10
- package/src/launcher/internal/runTtscserver.ts +9 -1
- package/src/launcher/internal/runTtsx.ts +33 -8
- package/src/plugin/internal/buildSourcePlugin.ts +40 -14
- package/src/plugin/internal/loadProjectPlugins.ts +42 -18
- package/src/structures/ITtscCompilerTransformation.ts +15 -2
- package/src/structures/ITtscPlugin.ts +0 -1
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +0 -2
- package/tools/gen_shims/main.go +43 -8
- package/utility/host.go +294 -245
package/driver/rewrite.go
CHANGED
|
@@ -14,34 +14,34 @@
|
|
|
14
14
|
package driver
|
|
15
15
|
|
|
16
16
|
import (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
"context"
|
|
18
|
+
"errors"
|
|
19
|
+
"fmt"
|
|
20
|
+
"os"
|
|
21
|
+
"path/filepath"
|
|
22
|
+
"regexp"
|
|
23
|
+
"strings"
|
|
24
|
+
"sync"
|
|
25
|
+
"unicode"
|
|
26
|
+
|
|
27
|
+
"github.com/microsoft/typescript-go/shim/ast"
|
|
28
|
+
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
// Rewrite describes one emit-time patch. Produced by CollectCallSites after
|
|
32
32
|
// the engine has generated a replacement JS fragment for the call.
|
|
33
33
|
type Rewrite struct {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
File *ast.SourceFile
|
|
35
|
+
RootName string
|
|
36
|
+
Namespaces []string
|
|
37
|
+
Method string
|
|
38
|
+
Replacement string
|
|
39
|
+
ConsumeParens bool
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// RewriteSet groups rewrites by file, preserving source order.
|
|
43
43
|
type RewriteSet struct {
|
|
44
|
-
|
|
44
|
+
byPath map[string][]Rewrite
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// NewRewriteSet returns an empty set.
|
|
@@ -49,20 +49,20 @@ func NewRewriteSet() *RewriteSet { return &RewriteSet{byPath: map[string][]Rewri
|
|
|
49
49
|
|
|
50
50
|
// Add registers a rewrite under the absolute path of its source file.
|
|
51
51
|
func (rs *RewriteSet) Add(r Rewrite) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
if r.File == nil {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
path := filepath.ToSlash(r.File.FileName())
|
|
56
|
+
rs.byPath[path] = append(rs.byPath[path], r)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Len returns the total number of rewrites across every file.
|
|
60
60
|
func (rs *RewriteSet) Len() int {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
n := 0
|
|
62
|
+
for _, rws := range rs.byPath {
|
|
63
|
+
n += len(rws)
|
|
64
|
+
}
|
|
65
|
+
return n
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// RewriteSentinel is the marker inserted at the top of a patched file so
|
|
@@ -74,127 +74,135 @@ const RewriteSentinel = "/* @ttsc-rewritten */"
|
|
|
74
74
|
// `writeFile` is nil, the patched JS is written to disk via the standard
|
|
75
75
|
// tsgo WriteFile.
|
|
76
76
|
func (p *Program) EmitAll(rs *RewriteSet, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
|
|
77
|
-
|
|
77
|
+
return p.emit(rs, nil, writeFile)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// EmitAllRaw runs TypeScript-Go emit without ttsc output-text rewrites.
|
|
81
81
|
func (p *Program) EmitAllRaw(writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
if p == nil || p.TSProgram == nil {
|
|
83
|
+
return nil, nil, errors.New("driver: nil program")
|
|
84
|
+
}
|
|
85
|
+
if err := p.ApplyLinkedPlugins(); err != nil {
|
|
86
|
+
return nil, nil, err
|
|
87
|
+
}
|
|
88
|
+
wf := writeFile
|
|
89
|
+
if wf == nil {
|
|
90
|
+
wf = func(fileName, text string, data *shimcompiler.WriteFileData) error {
|
|
91
|
+
return DefaultWriteFile(fileName, text)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
|
|
95
|
+
WriteFile: wf,
|
|
96
|
+
})
|
|
97
|
+
return result, convertDiagnostics(result.Diagnostics), nil
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// EmitFile runs tsgo's emitter for one source file, applying the same rewrite
|
|
101
101
|
// pipeline as EmitAll.
|
|
102
102
|
func (p *Program) EmitFile(rs *RewriteSet, target *ast.SourceFile, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
|
|
103
|
-
|
|
103
|
+
return p.emit(rs, target, writeFile)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
func (p *Program) emit(rs *RewriteSet, target *ast.SourceFile, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
|
|
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
|
-
|
|
107
|
+
if p == nil || p.TSProgram == nil {
|
|
108
|
+
return nil, nil, errors.New("driver: nil program")
|
|
109
|
+
}
|
|
110
|
+
if err := p.ApplyLinkedPlugins(); err != nil {
|
|
111
|
+
return nil, nil, err
|
|
112
|
+
}
|
|
113
|
+
if rs == nil {
|
|
114
|
+
rs = NewRewriteSet()
|
|
115
|
+
}
|
|
116
|
+
cursors := map[string]int{}
|
|
117
|
+
wf := func(fileName, text string, data *shimcompiler.WriteFileData) error {
|
|
118
|
+
// A patched file is idempotent: once the sentinel exists, the emitted text
|
|
119
|
+
// is passed through unchanged. This matters for watch/rebuild loops and
|
|
120
|
+
// tests that re-run emit over the same output directory.
|
|
121
|
+
if strings.Contains(text, RewriteSentinel) {
|
|
122
|
+
if writeFile != nil {
|
|
123
|
+
return writeFile(fileName, text, data)
|
|
124
|
+
}
|
|
125
|
+
return DefaultWriteFile(fileName, text)
|
|
126
|
+
}
|
|
127
|
+
// Rewrites are matched after tsgo has printed JavaScript. The source-file
|
|
128
|
+
// association is recovered from the output path because WriteFile receives
|
|
129
|
+
// only the final file name and text.
|
|
130
|
+
patched, err := applyRewrites(fileName, text, rs, cursors)
|
|
131
|
+
if err != nil {
|
|
132
|
+
return err
|
|
133
|
+
}
|
|
134
|
+
if patched != text {
|
|
135
|
+
patched = insertSentinel(patched)
|
|
136
|
+
}
|
|
137
|
+
if writeFile != nil {
|
|
138
|
+
return writeFile(fileName, patched, data)
|
|
139
|
+
}
|
|
140
|
+
return DefaultWriteFile(fileName, patched)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
|
|
144
|
+
TargetSourceFile: target,
|
|
145
|
+
WriteFile: wf,
|
|
146
|
+
})
|
|
147
|
+
return result, convertDiagnostics(result.Diagnostics), nil
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// DefaultWriteFile is the default disk writer used when EmitAll's caller does not
|
|
151
151
|
// supply a custom WriteFile callback.
|
|
152
152
|
func DefaultWriteFile(fileName, text string) error {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
if dir := filepath.Dir(fileName); dir != "" {
|
|
154
|
+
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
155
|
+
return err
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return os.WriteFile(fileName, []byte(text), 0o644)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
// insertSentinel prepends RewriteSentinel to the output text. When the file
|
|
162
|
+
// starts with a "use strict" directive (either quote style), the sentinel is
|
|
163
|
+
// inserted after it so the directive remains the first statement — ES modules
|
|
164
|
+
// and bundlers expect it at position zero.
|
|
161
165
|
func insertSentinel(text string) string {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
for _, prefix := range []string{"\"use strict\";\n", "'use strict';\n"} {
|
|
167
|
+
if strings.HasPrefix(text, prefix) {
|
|
168
|
+
return prefix + RewriteSentinel + "\n" + text[len(prefix):]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return RewriteSentinel + "\n" + text
|
|
168
172
|
}
|
|
169
173
|
|
|
174
|
+
// applyRewrites applies all registered rewrites for the source file that
|
|
175
|
+
// corresponds to outputName. cursors tracks how many rewrites have already
|
|
176
|
+
// been applied per source path across multiple WriteFile calls so incremental
|
|
177
|
+
// watch rebuilds resume at the right offset rather than re-scanning from zero.
|
|
170
178
|
func applyRewrites(outputName, text string, rs *RewriteSet, cursors map[string]int) (string, error) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
179
|
+
srcPath, ok := findSourceForOutput(outputName, rs)
|
|
180
|
+
if !ok || len(rs.byPath[srcPath]) == 0 {
|
|
181
|
+
return text, nil
|
|
182
|
+
}
|
|
183
|
+
rewrites := rs.byPath[srcPath]
|
|
184
|
+
pos := cursors[srcPath]
|
|
185
|
+
out := text
|
|
186
|
+
searchFrom := 0
|
|
187
|
+
for pos < len(rewrites) {
|
|
188
|
+
r := rewrites[pos]
|
|
189
|
+
replaced, nextSearchFrom, ok, err := spliceCall(out, r, searchFrom)
|
|
190
|
+
if err != nil {
|
|
191
|
+
return "", err
|
|
192
|
+
}
|
|
193
|
+
if !ok {
|
|
194
|
+
preview := out
|
|
195
|
+
if len(preview) > 400 {
|
|
196
|
+
preview = preview[:400] + "…"
|
|
197
|
+
}
|
|
198
|
+
return "", fmt.Errorf("driver: could not locate %s.%s(…) call in %s (tried roots %v; preview: %q)", joinRootAndNamespaces(r), r.Method, outputName, candidateRoots(r.RootName), preview)
|
|
199
|
+
}
|
|
200
|
+
out = replaced
|
|
201
|
+
searchFrom = nextSearchFrom
|
|
202
|
+
pos++
|
|
203
|
+
}
|
|
204
|
+
cursors[srcPath] = pos
|
|
205
|
+
return out, nil
|
|
198
206
|
}
|
|
199
207
|
|
|
200
208
|
// findSourceForOutput recovers which registered source file produced a given
|
|
@@ -213,93 +221,99 @@ func applyRewrites(outputName, text string, rs *RewriteSet, cursors map[string]i
|
|
|
213
221
|
// Ambiguous matches (two or more registered sources with the same tail) return
|
|
214
222
|
// no match so the caller treats the output as having no rewrites.
|
|
215
223
|
func findSourceForOutput(outputName string, rs *RewriteSet) (string, bool) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
224
|
+
if len(rs.byPath) == 0 {
|
|
225
|
+
return "", false
|
|
226
|
+
}
|
|
227
|
+
outStem := strings.TrimSuffix(filepath.ToSlash(outputName), filepath.Ext(outputName))
|
|
228
|
+
commonDir := commonSourceDirectoryFor(rs)
|
|
229
|
+
var matched string
|
|
230
|
+
hits := 0
|
|
231
|
+
for srcPath := range rs.byPath {
|
|
232
|
+
tail := sourceTail(srcPath, commonDir)
|
|
233
|
+
if tail == "" {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
if outStem == tail || strings.HasSuffix(outStem, "/"+tail) {
|
|
237
|
+
matched = srcPath
|
|
238
|
+
hits++
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if hits != 1 {
|
|
242
|
+
return "", false
|
|
243
|
+
}
|
|
244
|
+
return matched, true
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
// commonSourceDirectoryFor returns the deepest directory (with trailing "/")
|
|
240
248
|
// shared by every source path in rs.byPath. When rs has a single source this is
|
|
241
249
|
// just that source's directory.
|
|
242
250
|
func commonSourceDirectoryFor(rs *RewriteSet) string {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
251
|
+
var dirs [][]string
|
|
252
|
+
for srcPath := range rs.byPath {
|
|
253
|
+
dirs = append(dirs, strings.Split(filepath.ToSlash(filepath.Dir(srcPath)), "/"))
|
|
254
|
+
}
|
|
255
|
+
if len(dirs) == 0 {
|
|
256
|
+
return ""
|
|
257
|
+
}
|
|
258
|
+
common := dirs[0]
|
|
259
|
+
for _, other := range dirs[1:] {
|
|
260
|
+
n := len(common)
|
|
261
|
+
if len(other) < n {
|
|
262
|
+
n = len(other)
|
|
263
|
+
}
|
|
264
|
+
shared := 0
|
|
265
|
+
for i := 0; i < n; i++ {
|
|
266
|
+
if common[i] != other[i] {
|
|
267
|
+
break
|
|
268
|
+
}
|
|
269
|
+
shared++
|
|
270
|
+
}
|
|
271
|
+
common = common[:shared]
|
|
272
|
+
if len(common) == 0 {
|
|
273
|
+
break
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if len(common) == 0 {
|
|
277
|
+
return ""
|
|
278
|
+
}
|
|
279
|
+
return strings.Join(common, "/") + "/"
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
// sourceTail returns the source stem (extension dropped) without the common
|
|
275
283
|
// directory prefix. The leading "/" is also stripped so callers can match it as
|
|
276
284
|
// a suffix segment.
|
|
277
285
|
func sourceTail(srcPath, commonDir string) string {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
286
|
+
stem := strings.TrimSuffix(filepath.ToSlash(srcPath), filepath.Ext(srcPath))
|
|
287
|
+
if commonDir != "" && strings.HasPrefix(stem, commonDir) {
|
|
288
|
+
return stem[len(commonDir):]
|
|
289
|
+
}
|
|
290
|
+
return strings.TrimPrefix(stem, "/")
|
|
283
291
|
}
|
|
284
292
|
|
|
293
|
+
// spliceCall locates the next call expression for r in text starting at
|
|
294
|
+
// searchFrom and splices in r.Replacement. When ConsumeParens is true the
|
|
295
|
+
// replacement covers the entire call including arguments; otherwise only the
|
|
296
|
+
// head (root.namespaces.method) is replaced and the argument list is kept.
|
|
297
|
+
// Returns the patched text, the byte position to resume from on the next
|
|
298
|
+
// call, a found flag, and any error from the paren-matching step.
|
|
285
299
|
func spliceCall(text string, r Rewrite, searchFrom int) (string, int, bool, error) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
aliases := candidateRoots(r.RootName)
|
|
301
|
+
pattern := callRegexFor(aliases, r.Namespaces, r.Method)
|
|
302
|
+
idx, needleLen := findCallMatch(text, pattern, searchFrom)
|
|
303
|
+
if idx < 0 {
|
|
304
|
+
return text, searchFrom, false, nil
|
|
305
|
+
}
|
|
306
|
+
parenPos := idx + needleLen
|
|
307
|
+
closePos, ok := matchParen(text, parenPos)
|
|
308
|
+
if !ok {
|
|
309
|
+
return text, searchFrom, false, errors.New("driver: unbalanced parens while locating plugin call")
|
|
310
|
+
}
|
|
311
|
+
if r.ConsumeParens {
|
|
312
|
+
replaced := text[:idx] + r.Replacement + text[closePos+1:]
|
|
313
|
+
return replaced, idx + len(r.Replacement), true, nil
|
|
314
|
+
}
|
|
315
|
+
replaced := text[:idx] + r.Replacement + text[idx+needleLen:]
|
|
316
|
+
return replaced, idx + len(r.Replacement), true, nil
|
|
303
317
|
}
|
|
304
318
|
|
|
305
319
|
// callRegexFor compiles the loose-match needle pattern used by spliceCall.
|
|
@@ -317,30 +331,30 @@ func spliceCall(text string, r Rewrite, searchFrom int) (string, int, bool, erro
|
|
|
317
331
|
// same rewrite descriptor is re-checked once per emitted file in incremental
|
|
318
332
|
// watch builds.
|
|
319
333
|
func callRegexFor(aliases, namespaces []string, method string) *regexp.Regexp {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
334
|
+
key := strings.Join(aliases, "|") + "\x00" + strings.Join(namespaces, ".") + "\x00" + method
|
|
335
|
+
if cached, ok := callRegexCache.Load(key); ok {
|
|
336
|
+
return cached.(*regexp.Regexp)
|
|
337
|
+
}
|
|
338
|
+
rootAlternation := make([]string, 0, len(aliases))
|
|
339
|
+
for _, alias := range aliases {
|
|
340
|
+
rootAlternation = append(rootAlternation, regexp.QuoteMeta(alias))
|
|
341
|
+
}
|
|
342
|
+
var b strings.Builder
|
|
343
|
+
b.WriteString(`(?:`)
|
|
344
|
+
b.WriteString(strings.Join(rootAlternation, `|`))
|
|
345
|
+
b.WriteString(`)`)
|
|
346
|
+
for _, ns := range namespaces {
|
|
347
|
+
b.WriteString(`\s*\.\s*`)
|
|
348
|
+
b.WriteString(regexp.QuoteMeta(ns))
|
|
349
|
+
}
|
|
350
|
+
b.WriteString(`\s*\.\s*`)
|
|
351
|
+
b.WriteString(regexp.QuoteMeta(method))
|
|
352
|
+
b.WriteString(`\s*(\()`)
|
|
353
|
+
// MustCompile is safe because every contributing string was QuoteMeta'd
|
|
354
|
+
// and the surrounding template is a fixed regex grammar.
|
|
355
|
+
re := regexp.MustCompile(b.String())
|
|
356
|
+
callRegexCache.Store(key, re)
|
|
357
|
+
return re
|
|
344
358
|
}
|
|
345
359
|
|
|
346
360
|
var callRegexCache sync.Map
|
|
@@ -351,197 +365,245 @@ var callRegexCache sync.Map
|
|
|
351
365
|
// like `mytypia.foo(` don't shadow `typia.foo(`. Returns the start byte of the
|
|
352
366
|
// match and the length up to (but not including) the captured `(`.
|
|
353
367
|
func findCallMatch(text string, pattern *regexp.Regexp, searchFrom int) (int, int) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
start := searchFrom
|
|
369
|
+
if start < 0 {
|
|
370
|
+
start = 0
|
|
371
|
+
}
|
|
372
|
+
for start <= len(text) {
|
|
373
|
+
loc := pattern.FindStringSubmatchIndex(text[start:])
|
|
374
|
+
if loc == nil {
|
|
375
|
+
return -1, 0
|
|
376
|
+
}
|
|
377
|
+
matchStart := start + loc[0]
|
|
378
|
+
parenStart := start + loc[2]
|
|
379
|
+
if matchStart > 0 && isIdentifierPart(rune(text[matchStart-1])) {
|
|
380
|
+
start = matchStart + 1
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
return matchStart, parenStart - matchStart
|
|
384
|
+
}
|
|
385
|
+
return -1, 0
|
|
372
386
|
}
|
|
373
387
|
|
|
388
|
+
// candidateRoots expands a bare module name into the set of identifier stems
|
|
389
|
+
// tsgo's CommonJS emitter may produce for it. For example "typia" becomes
|
|
390
|
+
// ["typia", "typia_1.default", "typia_2.default", "typia.default",
|
|
391
|
+
// "typia_1", "typia_2"]. The regex alternation built from these candidates
|
|
392
|
+
// matches the emitted call regardless of which deduplication suffix tsgo chose.
|
|
374
393
|
func candidateRoots(root string) []string {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
394
|
+
return []string{
|
|
395
|
+
root,
|
|
396
|
+
root + "_1.default",
|
|
397
|
+
root + "_2.default",
|
|
398
|
+
root + ".default",
|
|
399
|
+
root + "_1",
|
|
400
|
+
root + "_2",
|
|
401
|
+
}
|
|
383
402
|
}
|
|
384
403
|
|
|
404
|
+
// joinRootAndNamespaces returns the human-readable "root.ns1.ns2" form of
|
|
405
|
+
// the rewrite's call head, used in error messages only.
|
|
385
406
|
func joinRootAndNamespaces(r Rewrite) string {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
407
|
+
if len(r.Namespaces) == 0 {
|
|
408
|
+
return r.RootName
|
|
409
|
+
}
|
|
410
|
+
return r.RootName + "." + strings.Join(r.Namespaces, ".")
|
|
390
411
|
}
|
|
391
412
|
|
|
413
|
+
// needleTail returns the literal suffix of a call expression head, e.g.
|
|
414
|
+
// ".ns.method(". This was the original literal-search needle before the regex
|
|
415
|
+
// rewriter was introduced; it is kept for potential future use by callers
|
|
416
|
+
// that do a quick pre-filter before invoking the regex path.
|
|
392
417
|
func needleTail(r Rewrite) string {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
418
|
+
if len(r.Namespaces) == 0 {
|
|
419
|
+
return "." + r.Method + "("
|
|
420
|
+
}
|
|
421
|
+
return "." + strings.Join(r.Namespaces, ".") + "." + r.Method + "("
|
|
397
422
|
}
|
|
398
423
|
|
|
424
|
+
// isIdentifierPart reports whether r can appear inside a JavaScript identifier.
|
|
425
|
+
// Used to ensure a regex match does not begin mid-identifier (e.g. "mytypia"
|
|
426
|
+
// must not match as "typia").
|
|
399
427
|
func isIdentifierPart(r rune) bool {
|
|
400
|
-
|
|
428
|
+
return r == '_' || r == '$' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
|
401
429
|
}
|
|
402
430
|
|
|
431
|
+
// matchParen finds the closing ")" that matches the "(" at text[pos],
|
|
432
|
+
// skipping over nested parentheses, strings, template literals, comments, and
|
|
433
|
+
// regex literals. Returns the byte index of the closing ")" and true on
|
|
434
|
+
// success; (0, false) when pos does not point at "(" or the text ends before
|
|
435
|
+
// the paren is closed. The lastSignificant variable tracks the most recently
|
|
436
|
+
// seen non-whitespace byte so canStartRegexLiteral can distinguish division
|
|
437
|
+
// from a regex literal opener.
|
|
403
438
|
func matchParen(text string, pos int) (int, bool) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
439
|
+
if pos >= len(text) || text[pos] != '(' {
|
|
440
|
+
return 0, false
|
|
441
|
+
}
|
|
442
|
+
depth := 1
|
|
443
|
+
lastSignificant := byte('(')
|
|
444
|
+
for i := pos + 1; i < len(text); i++ {
|
|
445
|
+
ch := text[i]
|
|
446
|
+
switch ch {
|
|
447
|
+
case ' ', '\t', '\n', '\r', '\f':
|
|
448
|
+
continue
|
|
449
|
+
case '(':
|
|
450
|
+
depth++
|
|
451
|
+
lastSignificant = ch
|
|
452
|
+
case ')':
|
|
453
|
+
depth--
|
|
454
|
+
if depth == 0 {
|
|
455
|
+
return i, true
|
|
456
|
+
}
|
|
457
|
+
lastSignificant = ch
|
|
458
|
+
case '"', '\'':
|
|
459
|
+
end, ok := skipQuoted(text, i, ch)
|
|
460
|
+
if !ok {
|
|
461
|
+
return 0, false
|
|
462
|
+
}
|
|
463
|
+
i = end
|
|
464
|
+
lastSignificant = 'x'
|
|
465
|
+
case '`':
|
|
466
|
+
end, ok := skipTemplate(text, i)
|
|
467
|
+
if !ok {
|
|
468
|
+
return 0, false
|
|
469
|
+
}
|
|
470
|
+
i = end
|
|
471
|
+
lastSignificant = 'x'
|
|
472
|
+
case '/':
|
|
473
|
+
if i+1 < len(text) {
|
|
474
|
+
switch text[i+1] {
|
|
475
|
+
case '/':
|
|
476
|
+
i = skipLineComment(text, i+2)
|
|
477
|
+
continue
|
|
478
|
+
case '*':
|
|
479
|
+
end, ok := skipBlockComment(text, i+2)
|
|
480
|
+
if !ok {
|
|
481
|
+
return 0, false
|
|
482
|
+
}
|
|
483
|
+
i = end
|
|
484
|
+
continue
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if canStartRegexLiteral(lastSignificant) {
|
|
488
|
+
end, ok := skipRegexLiteral(text, i)
|
|
489
|
+
if !ok {
|
|
490
|
+
return 0, false
|
|
491
|
+
}
|
|
492
|
+
i = end
|
|
493
|
+
lastSignificant = 'x'
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
lastSignificant = ch
|
|
497
|
+
default:
|
|
498
|
+
lastSignificant = ch
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return 0, false
|
|
467
502
|
}
|
|
468
503
|
|
|
504
|
+
// skipQuoted advances past a single- or double-quoted string literal starting
|
|
505
|
+
// at pos. Returns the index of the closing quote and true, or (0, false) on
|
|
506
|
+
// unterminated literals. Newlines inside a non-template string are illegal in
|
|
507
|
+
// JS and also terminate the scan as a failure.
|
|
469
508
|
func skipQuoted(text string, pos int, quote byte) (int, bool) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
509
|
+
for i := pos + 1; i < len(text); i++ {
|
|
510
|
+
switch text[i] {
|
|
511
|
+
case '\\':
|
|
512
|
+
i++
|
|
513
|
+
case quote:
|
|
514
|
+
return i, true
|
|
515
|
+
case '\n', '\r':
|
|
516
|
+
return 0, false
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return 0, false
|
|
481
520
|
}
|
|
482
521
|
|
|
522
|
+
// skipTemplate advances past a backtick template literal starting at pos.
|
|
523
|
+
// Nested template expressions (${...}) are not recursed into — the rewriter
|
|
524
|
+
// only needs to balance the outer backtick so it does not misinterpret a
|
|
525
|
+
// backtick inside the template as the end of a surrounding construct.
|
|
483
526
|
func skipTemplate(text string, pos int) (int, bool) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
527
|
+
for i := pos + 1; i < len(text); i++ {
|
|
528
|
+
switch text[i] {
|
|
529
|
+
case '\\':
|
|
530
|
+
i++
|
|
531
|
+
case '`':
|
|
532
|
+
return i, true
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return 0, false
|
|
493
536
|
}
|
|
494
537
|
|
|
538
|
+
// skipLineComment advances past a "//" line comment starting at pos (pos
|
|
539
|
+
// should be the character after the second "/"). Returns the index of the
|
|
540
|
+
// line terminator, or the last valid index when no newline is found.
|
|
495
541
|
func skipLineComment(text string, pos int) int {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
542
|
+
for i := pos; i < len(text); i++ {
|
|
543
|
+
if text[i] == '\n' || text[i] == '\r' {
|
|
544
|
+
return i
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return len(text) - 1
|
|
502
548
|
}
|
|
503
549
|
|
|
550
|
+
// skipBlockComment advances past a "/* … */" block comment starting at pos
|
|
551
|
+
// (pos should be the character after the opening "/*"). Returns the index of
|
|
552
|
+
// the closing "/" and true, or (0, false) when the comment is unterminated.
|
|
504
553
|
func skipBlockComment(text string, pos int) (int, bool) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
554
|
+
for i := pos; i+1 < len(text); i++ {
|
|
555
|
+
if text[i] == '*' && text[i+1] == '/' {
|
|
556
|
+
return i + 1, true
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return 0, false
|
|
511
560
|
}
|
|
512
561
|
|
|
562
|
+
// canStartRegexLiteral reports whether the byte previous (the last
|
|
563
|
+
// non-whitespace character seen before a "/") allows a regex literal to
|
|
564
|
+
// start. This is the minimal set of characters that unambiguously precede a
|
|
565
|
+
// regex in emitted CommonJS output; false positives are safe (they cause an
|
|
566
|
+
// extra skipRegexLiteral attempt that quickly fails), false negatives would
|
|
567
|
+
// misparse a "/" as the start of a comment or regex.
|
|
513
568
|
func canStartRegexLiteral(previous byte) bool {
|
|
514
|
-
|
|
569
|
+
return strings.ContainsRune("([{=,:;!&|?+-*~^<>%", rune(previous))
|
|
515
570
|
}
|
|
516
571
|
|
|
572
|
+
// skipRegexLiteral advances past a "/" regex literal starting at pos.
|
|
573
|
+
// Character classes ("[…]") are tracked so a "/" inside them is not treated
|
|
574
|
+
// as the closing delimiter. Returns the index of the last flag character (or
|
|
575
|
+
// the closing "/" when no flags follow) and true, or (0, false) for unterminated
|
|
576
|
+
// or newline-terminated literals.
|
|
517
577
|
func skipRegexLiteral(text string, pos int) (int, bool) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
578
|
+
inClass := false
|
|
579
|
+
for i := pos + 1; i < len(text); i++ {
|
|
580
|
+
switch text[i] {
|
|
581
|
+
case '\\':
|
|
582
|
+
i++
|
|
583
|
+
case '[':
|
|
584
|
+
inClass = true
|
|
585
|
+
case ']':
|
|
586
|
+
inClass = false
|
|
587
|
+
case '/':
|
|
588
|
+
if inClass {
|
|
589
|
+
continue
|
|
590
|
+
}
|
|
591
|
+
for i+1 < len(text) && isRegexFlag(text[i+1]) {
|
|
592
|
+
i++
|
|
593
|
+
}
|
|
594
|
+
return i, true
|
|
595
|
+
case '\n', '\r':
|
|
596
|
+
return 0, false
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return 0, false
|
|
540
600
|
}
|
|
541
601
|
|
|
602
|
+
// isRegexFlag reports whether ch is a valid regex flag character (letter,
|
|
603
|
+
// digit, "_" or "$"). ES2025 allows any IdentifierPart after the closing "/".
|
|
542
604
|
func isRegexFlag(ch byte) bool {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
605
|
+
return ch == '_' ||
|
|
606
|
+
ch == '$' ||
|
|
607
|
+
unicode.IsLetter(rune(ch)) ||
|
|
608
|
+
unicode.IsDigit(rune(ch))
|
|
547
609
|
}
|