@smoothbricks/lmao-ttsc 0.0.0-bootstrap.0 → 0.1.7
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/dist/bun-register.d.ts +2 -0
- package/dist/bun-register.d.ts.map +1 -0
- package/dist/bun-register.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/package.json +113 -4
- package/plugin/go.mod +9 -0
- package/plugin/loginline.go +296 -0
- package/plugin/main.go +572 -0
- package/plugin/main_test.go +43 -0
- package/plugin/optimization.go +462 -0
- package/plugin/resultinline.go +211 -0
- package/plugin/taginline.go +453 -0
- package/plugin/template_test.go +264 -0
- package/plugin.cjs +19 -0
- package/plugin.d.cts +5 -0
- package/README.md +0 -3
package/plugin/main.go
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
// ttsc transform plugin for LMAO — Go port of spec 01o (smoo/lmao!n/transformer).
|
|
2
|
+
//
|
|
3
|
+
// Implements the structural (TypeChecker-free) transformations:
|
|
4
|
+
//
|
|
5
|
+
// §1/§2 span() line injection + monomorphic spanN rewrite (heuristic Op detection)
|
|
6
|
+
// §5 defineModule() metadata injection (git_sha, package_name, package_file)
|
|
7
|
+
// §6 .line(N) injection on log/ok/err chains
|
|
8
|
+
// §7 task('name', fn) line injection
|
|
9
|
+
//
|
|
10
|
+
// NOT yet ported (staged, requires the tsgo Checker via driver shims):
|
|
11
|
+
//
|
|
12
|
+
// §3 destructured-context rewriting (shipped in the TS transformer;
|
|
13
|
+
// needs identifier-binding analysis parity before porting)
|
|
14
|
+
// §4 tag-chain inlining with schema specialization (enum indices,
|
|
15
|
+
// eager/lazy null-bitmap elision). The checker-free fallback of §4
|
|
16
|
+
// is deliberately NOT ported either: emitting direct buffer writes
|
|
17
|
+
// without the schema risks divergence from the TS inliner's output;
|
|
18
|
+
// run the classic transformer for tag inlining until the Checker
|
|
19
|
+
// port lands.
|
|
20
|
+
//
|
|
21
|
+
// Column-name contract (spec 01e): any future §4 port must write
|
|
22
|
+
// library-local (unprefixed) column names; prefix/mapColumns remapping is
|
|
23
|
+
// cold-path-only via RemappedBufferView and must never appear in emitted
|
|
24
|
+
// hot-path writes.
|
|
25
|
+
package main
|
|
26
|
+
|
|
27
|
+
import (
|
|
28
|
+
"encoding/json"
|
|
29
|
+
"fmt"
|
|
30
|
+
"os"
|
|
31
|
+
"os/exec"
|
|
32
|
+
"path/filepath"
|
|
33
|
+
"strconv"
|
|
34
|
+
"strings"
|
|
35
|
+
|
|
36
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
37
|
+
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
38
|
+
shimprinter "github.com/microsoft/typescript-go/shim/printer"
|
|
39
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
40
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const pluginName = "@smoothbricks/lmao-ttsc"
|
|
44
|
+
const pluginVersion = "0.1.6"
|
|
45
|
+
|
|
46
|
+
func main() {
|
|
47
|
+
if len(os.Args) < 2 {
|
|
48
|
+
fmt.Fprintf(os.Stderr, "%s: command required\n", pluginName)
|
|
49
|
+
os.Exit(2)
|
|
50
|
+
}
|
|
51
|
+
switch os.Args[1] {
|
|
52
|
+
case "version", "-v", "--version":
|
|
53
|
+
fmt.Printf("%s %s\n", pluginName, pluginVersion)
|
|
54
|
+
case "check":
|
|
55
|
+
// Transform-stage plugin: check is a no-op.
|
|
56
|
+
case "transform":
|
|
57
|
+
os.Exit(runTransform(os.Args[2:]))
|
|
58
|
+
case "build":
|
|
59
|
+
os.Exit(runBuild(os.Args[2:]))
|
|
60
|
+
default:
|
|
61
|
+
fmt.Fprintf(os.Stderr, "%s: unknown command %q\n", pluginName, os.Args[1])
|
|
62
|
+
os.Exit(2)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Host plumbing (per ttsc authoring walkthrough)
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
func readFlags(args []string) (cwd, tsconfig string) {
|
|
71
|
+
tsconfig = "tsconfig.json"
|
|
72
|
+
for _, a := range args {
|
|
73
|
+
switch {
|
|
74
|
+
case strings.HasPrefix(a, "--cwd="):
|
|
75
|
+
cwd = strings.TrimPrefix(a, "--cwd=")
|
|
76
|
+
case strings.HasPrefix(a, "--tsconfig="):
|
|
77
|
+
tsconfig = strings.TrimPrefix(a, "--tsconfig=")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if cwd == "" {
|
|
81
|
+
cwd, _ = os.Getwd()
|
|
82
|
+
}
|
|
83
|
+
if !filepath.IsAbs(tsconfig) {
|
|
84
|
+
tsconfig = filepath.Join(cwd, tsconfig)
|
|
85
|
+
}
|
|
86
|
+
return cwd, tsconfig
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type transformResult struct {
|
|
90
|
+
TypeScript map[string]string `json:"typescript"`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func outputKey(cwd, fileName string) string {
|
|
94
|
+
rel, err := filepath.Rel(cwd, fileName)
|
|
95
|
+
if err != nil {
|
|
96
|
+
return fileName
|
|
97
|
+
}
|
|
98
|
+
return filepath.ToSlash(rel)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// lmaoPluginTransform builds the emit-phase transformer. Running inside
|
|
102
|
+
// tsgo's per-file emit chain (the AST-integration model, same as typia) is
|
|
103
|
+
// what makes synthesized pos:-1 nodes safe: the checker's per-file emit-time
|
|
104
|
+
// passes (grammar checks, MarkLinkedReferences) run BEFORE the script
|
|
105
|
+
// transformers, so nothing type-checks the mutated tree. Checker queries the
|
|
106
|
+
// transform itself makes (tag-chain detection) happen in a collect phase over
|
|
107
|
+
// the still-untouched file before any node is spliced.
|
|
108
|
+
func lmaoPluginTransform(prog *driver.Program, cwd string) driver.PluginTransform {
|
|
109
|
+
return func(_ *shimprinter.EmitContext, sf *shimast.SourceFile) *shimast.SourceFile {
|
|
110
|
+
if sf == nil || sf.IsDeclarationFile {
|
|
111
|
+
return sf
|
|
112
|
+
}
|
|
113
|
+
t := &fileTransformer{
|
|
114
|
+
file: sf, cwd: cwd, checker: prog.Checker,
|
|
115
|
+
processed: map[*shimast.CallExpression]bool{},
|
|
116
|
+
opSpans: map[*shimast.CallExpression]bool{},
|
|
117
|
+
logTemplateIDs: map[*shimast.CallExpression]uint16{},
|
|
118
|
+
}
|
|
119
|
+
hintRewrites := t.collectOptimizations(sf.AsNode())
|
|
120
|
+
tagInlines, logInlines, resultInlines := t.collectTagInlines(sf.AsNode())
|
|
121
|
+
applyHintRewrites(hintRewrites)
|
|
122
|
+
t.applyTagInlines(tagInlines)
|
|
123
|
+
t.applyLogInlines(logInlines)
|
|
124
|
+
t.applyResultInlines(resultInlines)
|
|
125
|
+
t.walk(sf.AsNode())
|
|
126
|
+
// Wire Parent on synthesized nodes (only where nil); printer passes
|
|
127
|
+
// walk parents. See shim/ast/parent.go.
|
|
128
|
+
shimast.SetParentInChildrenUnset(sf.AsNode())
|
|
129
|
+
return sf
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func runTransform(args []string) int {
|
|
134
|
+
cwd, tsconfig := readFlags(args)
|
|
135
|
+
prog, _, err := driver.LoadProgram(cwd, tsconfig, driver.LoadProgramOptions{})
|
|
136
|
+
if err != nil {
|
|
137
|
+
fmt.Fprintf(os.Stderr, "%s: %v\n", pluginName, err)
|
|
138
|
+
return 2
|
|
139
|
+
}
|
|
140
|
+
defer prog.Close()
|
|
141
|
+
|
|
142
|
+
transform := lmaoPluginTransform(prog, cwd)
|
|
143
|
+
out := transformResult{TypeScript: map[string]string{}}
|
|
144
|
+
for _, file := range prog.SourceFiles() {
|
|
145
|
+
if file == nil || file.IsDeclarationFile {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
ec := shimprinter.NewEmitContext()
|
|
149
|
+
result := file
|
|
150
|
+
if next := transform(ec, result); next != nil {
|
|
151
|
+
result = next
|
|
152
|
+
}
|
|
153
|
+
printer := shimprinter.NewPrinter(shimprinter.PrinterOptions{}, shimprinter.PrintHandlers{}, ec)
|
|
154
|
+
out.TypeScript[outputKey(cwd, file.FileName())] = shimprinter.EmitSourceFile(printer, result)
|
|
155
|
+
}
|
|
156
|
+
data, err := json.Marshal(out)
|
|
157
|
+
if err != nil {
|
|
158
|
+
fmt.Fprintf(os.Stderr, "%s: marshal failed: %v\n", pluginName, err)
|
|
159
|
+
return 3
|
|
160
|
+
}
|
|
161
|
+
fmt.Println(string(data))
|
|
162
|
+
return 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func runBuild(args []string) int {
|
|
166
|
+
cwd, tsconfig := readFlags(args)
|
|
167
|
+
prog, _, err := driver.LoadProgram(cwd, tsconfig, driver.LoadProgramOptions{})
|
|
168
|
+
if err != nil {
|
|
169
|
+
fmt.Fprintf(os.Stderr, "%s: %v\n", pluginName, err)
|
|
170
|
+
return 2
|
|
171
|
+
}
|
|
172
|
+
defer prog.Close()
|
|
173
|
+
|
|
174
|
+
emitDiags, err := prog.EmitWithPluginTransformers(
|
|
175
|
+
[]driver.PluginTransform{lmaoPluginTransform(prog, cwd)}, nil)
|
|
176
|
+
if err != nil {
|
|
177
|
+
fmt.Fprintf(os.Stderr, "%s: emit failed: %v\n", pluginName, err)
|
|
178
|
+
return 3
|
|
179
|
+
}
|
|
180
|
+
if len(emitDiags) > 0 {
|
|
181
|
+
for _, d := range emitDiags {
|
|
182
|
+
fmt.Fprintln(os.Stderr, d.String())
|
|
183
|
+
}
|
|
184
|
+
return 2
|
|
185
|
+
}
|
|
186
|
+
return 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Node synthesis helpers (shared factory; synthesized nodes carry pos -1)
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
var factory = shimast.NewNodeFactory(shimast.NodeFactoryHooks{})
|
|
194
|
+
|
|
195
|
+
// Factory-built nodes keep their default undefined (-1) text range: that is
|
|
196
|
+
// what makes the printer emit .Text instead of a source span. The tsgo
|
|
197
|
+
// CHECKER, however, panics on pos -1 (checkGrammarNumericLiteral →
|
|
198
|
+
// GetTextOfNodeFromSourceText slices source text) — so the invariant here is
|
|
199
|
+
// ORDERING, not positions: every Checker query happens in a detection pass
|
|
200
|
+
// over the untouched parse tree, and synthesized nodes are only spliced in
|
|
201
|
+
// afterwards, where nothing type-checks them again.
|
|
202
|
+
|
|
203
|
+
func ident(text string) *shimast.Node {
|
|
204
|
+
return factory.NewIdentifier(text)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func str(text string) *shimast.Node {
|
|
208
|
+
return factory.NewStringLiteral(text, shimast.TokenFlagsNone)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func num(n int) *shimast.Node {
|
|
212
|
+
return factory.NewNumericLiteral(strconv.Itoa(n), shimast.TokenFlagsNone)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func propAccess(expr *shimast.Node, name string) *shimast.Node {
|
|
216
|
+
return factory.NewPropertyAccessExpression(expr, nil, ident(name), shimast.NodeFlagsNone)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func callExpr(expr *shimast.Node, args []*shimast.Node) *shimast.Node {
|
|
220
|
+
return factory.NewCallExpression(expr, nil, nil, factory.NewNodeList(args), shimast.NodeFlagsNone)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Transformations
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
var logMethods = map[string]bool{"info": true, "debug": true, "warn": true, "error": true, "trace": true}
|
|
228
|
+
var resultMethods = map[string]bool{"ok": true, "err": true}
|
|
229
|
+
|
|
230
|
+
type fileTransformer struct {
|
|
231
|
+
file *shimast.SourceFile
|
|
232
|
+
cwd string
|
|
233
|
+
// seenDefineModule guards the one-module-per-file invariant (spec 01o §5).
|
|
234
|
+
seenDefineModule bool
|
|
235
|
+
// processed marks call nodes produced by a chain rewrite so the walker
|
|
236
|
+
// does not re-match them (the Go analog of the TS transformer's
|
|
237
|
+
// processedCalls WeakSet). Without it, the cloned inner call re-matches
|
|
238
|
+
// on descent and the rewrite regresses infinitely.
|
|
239
|
+
processed map[*shimast.CallExpression]bool
|
|
240
|
+
// opSpans is populated with Checker-proved, stable-expression Op calls in
|
|
241
|
+
// the untouched-tree collect phase. The mutation walk never queries types.
|
|
242
|
+
opSpans map[*shimast.CallExpression]bool
|
|
243
|
+
// logTemplateIDs is populated by checker/provenance-gated per-Op analysis
|
|
244
|
+
// and consumed only after all checker-backed collection has completed.
|
|
245
|
+
logTemplateIDs map[*shimast.CallExpression]uint16
|
|
246
|
+
// checker is the program's pinned type checker (nil-safe: tag-chain
|
|
247
|
+
// inlining is skipped entirely without it).
|
|
248
|
+
checker *shimchecker.Checker
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func (t *fileTransformer) walk(node *shimast.Node) {
|
|
252
|
+
if node == nil {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
if node.Kind == shimast.KindCallExpression {
|
|
256
|
+
call := node.AsCallExpression()
|
|
257
|
+
switch {
|
|
258
|
+
case t.tryDefineModuleMetadata(call):
|
|
259
|
+
case t.processed[call]:
|
|
260
|
+
case t.trySpanRewrite(call):
|
|
261
|
+
case t.tryTaskLine(call):
|
|
262
|
+
case t.tryChainLine(call, logMethods, isLogReceiver):
|
|
263
|
+
case t.tryChainLine(call, resultMethods, nil):
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
267
|
+
t.walk(child)
|
|
268
|
+
return false
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// lineOf returns the 1-based line of a node's trivia-skipped start position
|
|
273
|
+
// (parity with the TS transformer's getStart()-based getLineNumber).
|
|
274
|
+
func (t *fileTransformer) lineOf(node *shimast.Node) int {
|
|
275
|
+
pos := shimscanner.SkipTrivia(t.file.Text(), node.Pos())
|
|
276
|
+
return shimscanner.GetECMALineOfPosition(t.file, pos) + 1
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// calleeNames returns the receiver and method name for `recv.name(...)`,
|
|
280
|
+
// or (nil, text) for a bare `name(...)`.
|
|
281
|
+
func calleeNames(call *shimast.CallExpression) (recv *shimast.Node, name string) {
|
|
282
|
+
expr := call.Expression
|
|
283
|
+
if expr.Kind == shimast.KindPropertyAccessExpression {
|
|
284
|
+
pa := expr.AsPropertyAccessExpression()
|
|
285
|
+
return pa.Expression, shimast.NodeText(pa.Name())
|
|
286
|
+
}
|
|
287
|
+
if expr.Kind == shimast.KindIdentifier {
|
|
288
|
+
return nil, shimast.NodeText(expr)
|
|
289
|
+
}
|
|
290
|
+
return nil, ""
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// --- §5 defineModule metadata ------------------------------------------------
|
|
294
|
+
|
|
295
|
+
func (t *fileTransformer) tryDefineModuleMetadata(call *shimast.CallExpression) bool {
|
|
296
|
+
_, name := calleeNames(call)
|
|
297
|
+
if name != "defineModule" || len(call.Arguments.Nodes) == 0 {
|
|
298
|
+
return false
|
|
299
|
+
}
|
|
300
|
+
arg := call.Arguments.Nodes[0]
|
|
301
|
+
if arg.Kind != shimast.KindObjectLiteralExpression {
|
|
302
|
+
return false
|
|
303
|
+
}
|
|
304
|
+
obj := arg.AsObjectLiteralExpression()
|
|
305
|
+
if t.seenDefineModule {
|
|
306
|
+
fmt.Fprintf(os.Stderr, "%s: invariant violation: %s contains multiple defineModule() declarations\n",
|
|
307
|
+
pluginName, t.file.FileName())
|
|
308
|
+
os.Exit(2)
|
|
309
|
+
}
|
|
310
|
+
t.seenDefineModule = true
|
|
311
|
+
for _, prop := range obj.Properties.Nodes {
|
|
312
|
+
if prop.Kind == shimast.KindPropertyAssignment && prop.Name() != nil &&
|
|
313
|
+
shimast.NodeText(prop.Name()) == "metadata" {
|
|
314
|
+
return false // already has metadata — leave alone
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
gitSha := gitLastCommit(t.file.FileName(), t.cwd)
|
|
319
|
+
pkgName, pkgFile := nearestPackage(t.file.FileName())
|
|
320
|
+
|
|
321
|
+
metaProps := []*shimast.Node{
|
|
322
|
+
factory.NewPropertyAssignment(nil, ident("git_sha"), nil, nil, str(gitSha)),
|
|
323
|
+
factory.NewPropertyAssignment(nil, ident("package_name"), nil, nil, str(pkgName)),
|
|
324
|
+
factory.NewPropertyAssignment(nil, ident("package_file"), nil, nil, str(pkgFile)),
|
|
325
|
+
}
|
|
326
|
+
metaObj := factory.NewObjectLiteralExpression(factory.NewNodeList(metaProps), true)
|
|
327
|
+
metadata := factory.NewPropertyAssignment(nil, ident("metadata"), nil, nil, metaObj)
|
|
328
|
+
|
|
329
|
+
obj.Properties = factory.NewNodeList(append([]*shimast.Node{metadata}, obj.Properties.Nodes...))
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
func gitLastCommit(filePath, cwd string) string {
|
|
334
|
+
rel, err := filepath.Rel(cwd, filePath)
|
|
335
|
+
if err != nil {
|
|
336
|
+
return "unknown"
|
|
337
|
+
}
|
|
338
|
+
out, err := exec.Command("git", "-C", cwd, "rev-list", "-1", "HEAD", "--", filepath.ToSlash(rel)).Output()
|
|
339
|
+
if err != nil {
|
|
340
|
+
return "unknown"
|
|
341
|
+
}
|
|
342
|
+
sha := strings.TrimSpace(string(out))
|
|
343
|
+
if sha == "" {
|
|
344
|
+
return "unknown"
|
|
345
|
+
}
|
|
346
|
+
return sha
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
func nearestPackage(filePath string) (name, relFile string) {
|
|
350
|
+
dir := filepath.Dir(filePath)
|
|
351
|
+
root := filepath.VolumeName(dir) + string(filepath.Separator)
|
|
352
|
+
for dir != root {
|
|
353
|
+
data, err := os.ReadFile(filepath.Join(dir, "package.json"))
|
|
354
|
+
if err == nil {
|
|
355
|
+
var pkg struct {
|
|
356
|
+
Name string `json:"name"`
|
|
357
|
+
}
|
|
358
|
+
if json.Unmarshal(data, &pkg) == nil && pkg.Name != "" {
|
|
359
|
+
rel, err := filepath.Rel(dir, filePath)
|
|
360
|
+
if err != nil {
|
|
361
|
+
rel = filepath.Base(filePath)
|
|
362
|
+
}
|
|
363
|
+
return pkg.Name, filepath.ToSlash(rel)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
dir = filepath.Dir(dir)
|
|
367
|
+
}
|
|
368
|
+
return "unknown", filepath.Base(filePath)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// --- §1/§2 span rewrite --------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
func (t *fileTransformer) trySpanRewrite(call *shimast.CallExpression) bool {
|
|
374
|
+
recv, name := calleeNames(call)
|
|
375
|
+
if name != "span" || recv == nil || len(call.Arguments.Nodes) < 2 || len(call.Arguments.Nodes) > 10 {
|
|
376
|
+
return false
|
|
377
|
+
}
|
|
378
|
+
args := call.Arguments.Nodes
|
|
379
|
+
nameArg, opOrFn := args[0], args[1]
|
|
380
|
+
rest := args[2:]
|
|
381
|
+
|
|
382
|
+
if recv.Kind != shimast.KindIdentifier && recv.Kind != shimast.KindThisKeyword {
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
isPlainFunction := opOrFn.Kind == shimast.KindArrowFunction || opOrFn.Kind == shimast.KindFunctionExpression
|
|
386
|
+
if !isPlainFunction && !t.opSpans[call] {
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
line := t.lineOf(call.AsNode())
|
|
391
|
+
methodName := fmt.Sprintf("span%d", len(rest))
|
|
392
|
+
childCtx := callExpr(propAccess(ident("Object"), "create"), []*shimast.Node{recv})
|
|
393
|
+
var bufferClass, remappedView, opMetadata, fn, runtimeHint *shimast.Node
|
|
394
|
+
if isPlainFunction {
|
|
395
|
+
bufferClass = propAccess(propAccess(recv, "_buffer"), "constructor")
|
|
396
|
+
remappedView = ident("undefined")
|
|
397
|
+
opMetadata = propAccess(propAccess(recv, "_buffer"), "_opMetadata")
|
|
398
|
+
fn = opOrFn
|
|
399
|
+
runtimeHint = num(0)
|
|
400
|
+
} else {
|
|
401
|
+
bufferClass = propAccess(opOrFn, "SpanBufferClass")
|
|
402
|
+
remappedView = propAccess(opOrFn, "remappedViewClass")
|
|
403
|
+
opMetadata = propAccess(opOrFn, "metadata")
|
|
404
|
+
fn = propAccess(opOrFn, "fn")
|
|
405
|
+
runtimeHint = propAccess(opOrFn, "runtimeHint")
|
|
406
|
+
}
|
|
407
|
+
newArgs := append([]*shimast.Node{
|
|
408
|
+
num(line), nameArg, childCtx, bufferClass, remappedView, opMetadata, fn,
|
|
409
|
+
}, rest...)
|
|
410
|
+
newArgs = append(newArgs, runtimeHint)
|
|
411
|
+
|
|
412
|
+
call.Expression = propAccess(recv, methodName)
|
|
413
|
+
call.Arguments = factory.NewNodeList(newArgs)
|
|
414
|
+
return true
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// --- §7 task line ---------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
func (t *fileTransformer) tryTaskLine(call *shimast.CallExpression) bool {
|
|
420
|
+
_, name := calleeNames(call)
|
|
421
|
+
if name != "task" || len(call.Arguments.Nodes) != 2 {
|
|
422
|
+
return false
|
|
423
|
+
}
|
|
424
|
+
if call.Arguments.Nodes[0].Kind != shimast.KindStringLiteral {
|
|
425
|
+
return false
|
|
426
|
+
}
|
|
427
|
+
call.Arguments = factory.NewNodeList(append(
|
|
428
|
+
append([]*shimast.Node{}, call.Arguments.Nodes...),
|
|
429
|
+
num(t.lineOf(call.AsNode())),
|
|
430
|
+
))
|
|
431
|
+
return true
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// --- §6 log / result .line(N) -----------------------------------------------------
|
|
435
|
+
|
|
436
|
+
// isLogReceiver checks the receiver of a log method is a `.log` property access.
|
|
437
|
+
func isLogReceiver(recv *shimast.Node) bool {
|
|
438
|
+
return recv != nil &&
|
|
439
|
+
recv.Kind == shimast.KindPropertyAccessExpression &&
|
|
440
|
+
shimast.NodeText(recv.AsPropertyAccessExpression().Name()) == "log"
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// tryChainLine inserts `.line(N)` right after the matched method at the root
|
|
444
|
+
// of a fluent chain, preserving trailing calls; no-op when `.line` is present.
|
|
445
|
+
func (t *fileTransformer) tryChainLine(call *shimast.CallExpression, methods map[string]bool, receiverOK func(*shimast.Node) bool) bool {
|
|
446
|
+
if t.processed[call] {
|
|
447
|
+
return false
|
|
448
|
+
}
|
|
449
|
+
// Only fire at the TOP of a fluent chain: a call that is itself the
|
|
450
|
+
// receiver of an enclosing method call belongs to a larger chain whose
|
|
451
|
+
// top the walker handles (or already handled). Injecting mid-chain
|
|
452
|
+
// duplicates .line() (the TS transformer prevents this with its
|
|
453
|
+
// processedCalls sweep over allCallsInChain).
|
|
454
|
+
if parent := call.AsNode().Parent; parent != nil && parent.Kind == shimast.KindPropertyAccessExpression {
|
|
455
|
+
if gp := parent.Parent; gp != nil && gp.Kind == shimast.KindCallExpression &&
|
|
456
|
+
gp.AsCallExpression().Expression == parent {
|
|
457
|
+
return false
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
target, trailing := findChainTarget(call, methods, receiverOK)
|
|
461
|
+
templateID := uint16(0)
|
|
462
|
+
if receiverOK != nil {
|
|
463
|
+
if templateTarget, templateTrailing, id := findTemplateLogInChain(call, t.logTemplateIDs); templateTarget != nil {
|
|
464
|
+
target, trailing, templateID = templateTarget, templateTrailing, id
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if target == nil {
|
|
468
|
+
return false
|
|
469
|
+
}
|
|
470
|
+
hasLine := chainHasLine(call)
|
|
471
|
+
if hasLine && templateID == 0 {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
line := t.lineOf(target.AsNode())
|
|
475
|
+
|
|
476
|
+
// Build a fresh inner call, substituting the private template entry point
|
|
477
|
+
// when this exact call was assigned an Op-local ID during collection.
|
|
478
|
+
innerExpression := target.Expression
|
|
479
|
+
innerArguments := target.Arguments
|
|
480
|
+
if templateID != 0 {
|
|
481
|
+
pa := target.Expression.AsPropertyAccessExpression()
|
|
482
|
+
innerExpression = propAccess(pa.Expression, "_"+shimast.NodeText(pa.Name())+"Template")
|
|
483
|
+
innerArguments = factory.NewNodeList([]*shimast.Node{num(int(templateID))})
|
|
484
|
+
}
|
|
485
|
+
inner := factory.NewCallExpression(
|
|
486
|
+
innerExpression, nil, target.TypeArguments, innerArguments, shimast.NodeFlagsNone,
|
|
487
|
+
)
|
|
488
|
+
t.processed[inner.AsCallExpression()] = true
|
|
489
|
+
t.processed[call] = true
|
|
490
|
+
var rebuilt *shimast.Node
|
|
491
|
+
if hasLine {
|
|
492
|
+
rebuilt = inner
|
|
493
|
+
} else {
|
|
494
|
+
rebuilt = callExpr(propAccess(inner, "line"), []*shimast.Node{num(line)})
|
|
495
|
+
}
|
|
496
|
+
for _, link := range trailing {
|
|
497
|
+
rebuilt = callExpr(propAccess(rebuilt, link.name), factory.NewNodeList(link.args).Nodes)
|
|
498
|
+
}
|
|
499
|
+
rc := rebuilt.AsCallExpression()
|
|
500
|
+
call.Expression = rc.Expression
|
|
501
|
+
call.Arguments = rc.Arguments
|
|
502
|
+
return true
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// findTemplateLogInChain locates a previously analyzed literal call while
|
|
506
|
+
// preserving the same receiver-to-outer fluent link order as findChainTarget.
|
|
507
|
+
func findTemplateLogInChain(call *shimast.CallExpression, ids map[*shimast.CallExpression]uint16) (*shimast.CallExpression, []chainLink, uint16) {
|
|
508
|
+
var trailing []chainLink
|
|
509
|
+
current := call
|
|
510
|
+
for {
|
|
511
|
+
if id := ids[current]; id != 0 {
|
|
512
|
+
return current, trailing, id
|
|
513
|
+
}
|
|
514
|
+
expr := current.Expression
|
|
515
|
+
if expr.Kind != shimast.KindPropertyAccessExpression {
|
|
516
|
+
return nil, nil, 0
|
|
517
|
+
}
|
|
518
|
+
pa := expr.AsPropertyAccessExpression()
|
|
519
|
+
trailing = append([]chainLink{{name: shimast.NodeText(pa.Name()), args: current.Arguments.Nodes}}, trailing...)
|
|
520
|
+
if pa.Expression.Kind != shimast.KindCallExpression {
|
|
521
|
+
return nil, nil, 0
|
|
522
|
+
}
|
|
523
|
+
current = pa.Expression.AsCallExpression()
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
type chainLink struct {
|
|
528
|
+
name string
|
|
529
|
+
args []*shimast.Node
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// findChainTarget walks receiver-wards from `call` looking for the first
|
|
533
|
+
// method whose name is in `methods` (and whose receiver passes receiverOK),
|
|
534
|
+
// collecting the trailing links crossed on the way.
|
|
535
|
+
func findChainTarget(call *shimast.CallExpression, methods map[string]bool, receiverOK func(*shimast.Node) bool) (*shimast.CallExpression, []chainLink) {
|
|
536
|
+
var trailing []chainLink
|
|
537
|
+
current := call
|
|
538
|
+
for {
|
|
539
|
+
expr := current.Expression
|
|
540
|
+
if expr.Kind != shimast.KindPropertyAccessExpression {
|
|
541
|
+
return nil, nil
|
|
542
|
+
}
|
|
543
|
+
pa := expr.AsPropertyAccessExpression()
|
|
544
|
+
name := shimast.NodeText(pa.Name())
|
|
545
|
+
if methods[name] && (receiverOK == nil || receiverOK(pa.Expression)) {
|
|
546
|
+
return current, trailing
|
|
547
|
+
}
|
|
548
|
+
trailing = append([]chainLink{{name: name, args: current.Arguments.Nodes}}, trailing...)
|
|
549
|
+
if pa.Expression.Kind != shimast.KindCallExpression {
|
|
550
|
+
return nil, nil
|
|
551
|
+
}
|
|
552
|
+
current = pa.Expression.AsCallExpression()
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
func chainHasLine(call *shimast.CallExpression) bool {
|
|
557
|
+
current := call
|
|
558
|
+
for {
|
|
559
|
+
expr := current.Expression
|
|
560
|
+
if expr.Kind != shimast.KindPropertyAccessExpression {
|
|
561
|
+
return false
|
|
562
|
+
}
|
|
563
|
+
pa := expr.AsPropertyAccessExpression()
|
|
564
|
+
if shimast.NodeText(pa.Name()) == "line" {
|
|
565
|
+
return true
|
|
566
|
+
}
|
|
567
|
+
if pa.Expression.Kind != shimast.KindCallExpression {
|
|
568
|
+
return false
|
|
569
|
+
}
|
|
570
|
+
current = pa.Expression.AsCallExpression()
|
|
571
|
+
}
|
|
572
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Unit tests for the pure transform logic, per ttsc's AGENTS.md §2.2 testing
|
|
2
|
+
// shape (one Go unit test for pure logic + one e2e spawning ttsc against a
|
|
3
|
+
// fixture — the e2e lives with the consumer fixture once ttsc is a dev dep).
|
|
4
|
+
//
|
|
5
|
+
// NOTE: cannot run in this repo (no Go toolchain in devenv); run in a
|
|
6
|
+
// Go-enabled environment: cd plugin && go test ./...
|
|
7
|
+
package main
|
|
8
|
+
|
|
9
|
+
import (
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"testing"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestNearestPackage(t *testing.T) {
|
|
15
|
+
// This file lives inside @smoothbricks/lmao-ttsc.
|
|
16
|
+
abs, err := filepath.Abs("main.go")
|
|
17
|
+
if err != nil {
|
|
18
|
+
t.Fatal(err)
|
|
19
|
+
}
|
|
20
|
+
name, rel := nearestPackage(abs)
|
|
21
|
+
if name != "@smoothbricks/lmao-ttsc" {
|
|
22
|
+
t.Fatalf("nearestPackage name = %q, want @smoothbricks/lmao-ttsc", name)
|
|
23
|
+
}
|
|
24
|
+
if rel != "plugin/main.go" {
|
|
25
|
+
t.Fatalf("nearestPackage rel = %q, want plugin/main.go", rel)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestGitLastCommitUnknownOutsideRepo(t *testing.T) {
|
|
30
|
+
if sha := gitLastCommit("/definitely/not/a/file.ts", "/tmp"); sha != "unknown" {
|
|
31
|
+
t.Fatalf("expected unknown, got %q", sha)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func TestReadFlags(t *testing.T) {
|
|
36
|
+
cwd, tsconfig := readFlags([]string{"--cwd=/x", "--tsconfig=custom.json"})
|
|
37
|
+
if cwd != "/x" {
|
|
38
|
+
t.Fatalf("cwd = %q", cwd)
|
|
39
|
+
}
|
|
40
|
+
if tsconfig != filepath.Join("/x", "custom.json") {
|
|
41
|
+
t.Fatalf("tsconfig = %q", tsconfig)
|
|
42
|
+
}
|
|
43
|
+
}
|