Skip to content

Commit d634051

Browse files
committed
Support import extraction for TS. Fix TypeScript templates.
1 parent ea410c6 commit d634051

23 files changed

+366
-108
lines changed

compiler/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func (p *optionsParser) parseFrom(file ast.File) {
107107
p.validLangs(opt.Key(), "go")
108108
opts.EventFields = p.parseExpr(opt.Value(), opts.EventFields).(bool)
109109
case "eventAST":
110-
p.validLangs(opt.Key(), "go")
110+
p.validLangs(opt.Key(), "go", "ts")
111111
opts.EventAST = p.parseExpr(opt.Value(), opts.EventAST).(bool)
112112
case "extraTypes":
113113
opts.ExtraTypes = p.parseExpr(opt.Value(), opts.ExtraTypes).([]syntax.ExtraType)

gen/gen.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ func Generate(g *grammar.Grammar, w Writer, opts Options) error {
148148
switch g.TargetLang {
149149
case "go":
150150
src = FormatGo(outName, ExtractGoImports(src))
151+
case "ts":
152+
src = ExtractTsImports(src)
151153
}
152154
if err := w.Write(outName, src); err != nil {
153155
return err

gen/post_test.go

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func TestFormat(t *testing.T) {
2525
}
2626
}
2727

28-
var importTests = []struct {
28+
var goImportTests = []struct {
2929
input string
3030
want string
3131
}{
@@ -51,15 +51,15 @@ var a = make(map[string]abc.Imp
5151
func f(q []foo.Temp) {}`},
5252
}
5353

54-
func TestExtractImports(t *testing.T) {
55-
for _, tc := range importTests {
54+
func TestExtractGoImports(t *testing.T) {
55+
for _, tc := range goImportTests {
5656
if got := gen.ExtractGoImports(tc.input); got != tc.want {
5757
t.Errorf("ExtractImports(%q) = %q, want: %q", tc.input, got, tc.want)
5858
}
5959
}
6060
}
6161

62-
func BenchmarkExtractImports(b *testing.B) {
62+
func BenchmarkExtractGoImports(b *testing.B) {
6363
const input = `package a
6464
6565
func TestExtractImports(t *"testing"".T) {
@@ -74,3 +74,123 @@ func TestExtractImports(t *"testing"".T) {
7474
}
7575
b.SetBytes(int64(len(input))) // ~44 MB/s
7676
}
77+
78+
var tsImportTests = []struct {
79+
name string
80+
input string
81+
want string
82+
}{
83+
{
84+
name: "single import",
85+
input: `
86+
const foo = "./foo".Bar;`,
87+
want: `
88+
import {Bar} from "./foo";
89+
90+
const foo = Bar;`,
91+
},
92+
{
93+
name: "skipping comments",
94+
input: `// abc
95+
// def
96+
const foo = "./foo".Bar;`,
97+
want: `// abc
98+
// def
99+
import {Bar} from "./foo";
100+
101+
const foo = Bar;`,
102+
},
103+
{
104+
name: "pre-existing imports",
105+
input: `// abc
106+
// def
107+
import {Baz} from "./foo2";
108+
const foo = "./foo".Bar;`,
109+
want: `// abc
110+
// def
111+
import {Bar} from "./foo";
112+
import {Baz} from "./foo2";
113+
const foo = Bar;`,
114+
},
115+
{
116+
name: "multiple imports from same module",
117+
input: `const x = "./foo".Bar;
118+
const y = "./foo".Baz;`,
119+
want: `import {Bar, Baz} from "./foo";
120+
121+
const x = Bar;
122+
const y = Baz;`,
123+
},
124+
{
125+
name: "imports from different modules",
126+
input: `const a = "./foo".Bar;
127+
const b = "./bar".Qux;`,
128+
want: `import {Qux} from "./bar";
129+
import {Bar} from "./foo";
130+
131+
const a = Bar;
132+
const b = Qux;`,
133+
},
134+
{
135+
name: "nested module paths",
136+
input: `const a = "./deep/nested/path".Component;`,
137+
want: `import {Component} from "./deep/nested/path";
138+
139+
const a = Component;`,
140+
},
141+
{
142+
name: "no imports",
143+
input: `const a = 5; function test() { return true; }`,
144+
want: `const a = 5; function test() { return true; }`,
145+
},
146+
{
147+
name: "function call with imported symbol",
148+
input: `function process() { return "./utils".formatData(data); }`,
149+
want: `import {formatData} from "./utils";
150+
151+
function process() { return formatData(data); }`,
152+
},
153+
{
154+
name: "import in a complex expression",
155+
input: `const handler = (event) => {
156+
"./events".EventBus.publish(new "./models".Event());
157+
};`,
158+
want: `import {EventBus} from "./events";
159+
import {Event} from "./models";
160+
161+
const handler = (event) => {
162+
EventBus.publish(new Event());
163+
};`,
164+
},
165+
}
166+
167+
func TestExtractTsImports(t *testing.T) {
168+
for _, tc := range tsImportTests {
169+
t.Run(tc.name, func(t *testing.T) {
170+
got := gen.ExtractTsImports(tc.input)
171+
if got != tc.want {
172+
t.Errorf("ExtractTsImports() =\n%s\nwant:\n%s", got, tc.want)
173+
}
174+
})
175+
}
176+
}
177+
178+
func BenchmarkExtractTsImports(b *testing.B) {
179+
const input = `const Component = "./components".Component;
180+
const { useState, useEffect } = "./react".React;
181+
function App() {
182+
const [data, setData] = useState(null);
183+
const formatter = new "./utils".Formatter();
184+
185+
useEffect(() => {
186+
"./api".fetchData().then(setData);
187+
}, []);
188+
189+
return formatter.format(data);
190+
}`
191+
192+
for i := 0; i < b.N; i++ {
193+
gen.ExtractTsImports(input)
194+
}
195+
b.SetBytes(int64(len(input)))
196+
}

gen/post_ts.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package gen
2+
3+
import (
4+
"regexp"
5+
"sort"
6+
"strings"
7+
)
8+
9+
var qualifierTsRE = regexp.MustCompile(`("([\w\/\.-]+)")\.(\w+)`)
10+
11+
// ExtractTsImports rewrites the content of a generated TypeScript file, deriving imports
12+
// from qualified names that can appear anywhere in src, where one can reference a symbol
13+
// from another module. The format:
14+
//
15+
// "./foo".Bar
16+
//
17+
// will be transformed into proper TypeScript imports:
18+
//
19+
// import {Bar} from "./foo"
20+
//
21+
// Multiple imports from the same module will be combined:
22+
//
23+
// "./foo".Bar, "./foo".Baz -> import {Bar, Baz} from "./foo"
24+
func ExtractTsImports(src string) string {
25+
var b strings.Builder
26+
byModule := make(map[string]map[string]bool) // module -> set of symbols
27+
28+
// First pass: collect all imports and transform the source
29+
for {
30+
match := qualifierTsRE.FindStringSubmatchIndex(src)
31+
if match == nil {
32+
break
33+
}
34+
35+
slice := func(n int) string {
36+
s := match[2*n]
37+
if s == -1 {
38+
return ""
39+
}
40+
return src[s:match[2*n+1]]
41+
}
42+
43+
modulePath := slice(2)
44+
symbol := slice(3)
45+
46+
// Add the symbol to the map of imports for this module
47+
if _, ok := byModule[modulePath]; !ok {
48+
byModule[modulePath] = make(map[string]bool)
49+
}
50+
byModule[modulePath][symbol] = true
51+
52+
// Write everything before the match
53+
b.WriteString(src[:match[0]])
54+
// Replace the "module".Symbol with just Symbol
55+
b.WriteString(symbol)
56+
57+
// Move past the match
58+
src = src[match[1]:]
59+
}
60+
61+
// Add the remaining source
62+
b.WriteString(src)
63+
64+
// If no imports were found, return the original source
65+
if len(byModule) == 0 {
66+
return b.String()
67+
}
68+
69+
// Sort modules to ensure consistent output
70+
var modules []string
71+
for m := range byModule {
72+
modules = append(modules, m)
73+
}
74+
sort.Strings(modules)
75+
76+
// Generate import statements
77+
var header strings.Builder
78+
for _, mod := range modules {
79+
var symbols []string
80+
for sym := range byModule[mod] {
81+
symbols = append(symbols, sym)
82+
}
83+
sort.Strings(symbols)
84+
85+
header.WriteString("import {")
86+
for i, sym := range symbols {
87+
if i > 0 {
88+
header.WriteString(", ")
89+
}
90+
header.WriteString(sym)
91+
}
92+
header.WriteString("} from \"")
93+
header.WriteString(mod)
94+
header.WriteString("\";\n")
95+
}
96+
97+
source := b.String()
98+
if header.Len() == 0 {
99+
return source
100+
}
101+
102+
var insert int
103+
for strings.HasPrefix(source[insert:], "//") {
104+
if nl := strings.Index(source[insert:], "\n"); nl != -1 {
105+
insert += nl + 1
106+
} else {
107+
break
108+
}
109+
}
110+
if strings.HasPrefix(source[insert:], "\n") {
111+
// Skip the blank line after the top-level comments.
112+
insert++
113+
}
114+
if !strings.HasPrefix(source[insert:], "import ") {
115+
header.WriteString("\n")
116+
}
117+
118+
// Combine the import statements with the transformed source
119+
return source[:insert] + header.String() + source[insert:]
120+
}

gen/templates.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ var languages = map[string]*language{
3535
AST: []file{
3636
{"ast/tree.go", builtin(`go_ast_tree`)},
3737
{"ast/parse.go", builtin(`go_ast_parse`)},
38+
},
39+
TypedAST: []file{
3840
{"ast/ast.go", builtin(`go_ast`)},
3941
{"ast/factory.go", builtin(`go_ast_factory`)},
4042
},
@@ -72,12 +74,14 @@ var languages = map[string]*language{
7274
Parser: []file{
7375
{"parser.ts", builtin("ts_parser")},
7476
{"parser_tables.ts", builtin("ts_parser_tables")},
75-
{"ast/tree.ts", builtin("ts_ast_tree")},
76-
{"ast/parse.ts", builtin("ts_ast_parse")},
7777
},
7878
Stream: []file{
7979
{"stream.ts", builtin(`ts_stream`)},
8080
},
81+
AST: []file{
82+
{"tree.ts", builtin("ts_tree")},
83+
{"builder.ts", builtin("ts_builder")},
84+
},
8185
},
8286
}
8387

@@ -93,6 +97,7 @@ type language struct {
9397
Types []file
9498
Selector []file
9599
AST []file
100+
TypedAST []file
96101
Bison []file
97102

98103
SharedDefs string
@@ -121,9 +126,12 @@ func (l *language) templates(g *grammar.Grammar) []file {
121126
if g.Options.GenSelector || g.Options.EventFields {
122127
ret = append(ret, l.Selector...)
123128
}
124-
if g.Options.EventFields && g.Options.EventAST {
129+
if g.Options.EventAST {
125130
ret = append(ret, l.AST...)
126131
}
132+
if g.Options.EventFields && g.Options.EventAST {
133+
ret = append(ret, l.TypedAST...)
134+
}
127135
}
128136
if g.Options.WriteBison {
129137
ret = append(ret, file{name: g.Name + ".y", template: bisonTpl})

gen/templates/ts_ast.go.tmpl

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{{ template "header" . -}}
22

3-
import { NodeType, {{if .Parser.UsedFlags}}{{template "nodeFlagsRef" .}}, {{end}}Listener } from '../listener'
4-
import { Parser, SyntaxError, ErrorMsg{{if .Parser.IsRecovering }}, ErrorHandler{{end}} } from '../parser'
5-
import { Selector } from '../selector';
3+
import { NodeType, {{if .Parser.UsedFlags}}{{template "nodeFlagsRef" .}}, {{end}}Listener } from './listener'
4+
import { Parser, SyntaxError, ErrorMsg{{if .Parser.IsRecovering }}, ErrorHandler{{end}} } from './parser'
5+
import { Selector } from './selector';
66
{{ if $.Options.TokenStream -}}
7-
import { TokenStream } from '../stream';
7+
import { TokenStream } from './stream';
88
{{ else -}}
9-
import { Lexer } from '../lexer';
9+
import { Lexer } from './lexer';
1010
{{ end -}}
1111
import { Tree, Node } from './tree'
1212
{{range $index, $inp := .Parser.Inputs -}}

gen/templates/ts_common.go.tmpl

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
{{- template "header" . -}}
22

3-
import * as {{template "tokenPkg"}} from './token';
4-
53
export const debugSyntax = {{ .Options.DebugParser }};
64

75
export function debugLog(...data: any[]) : void {
@@ -18,11 +16,11 @@ export function debugLog(...data: any[]) : void {
1816
{{- define "symbol" -}}
1917
{{ if .Options.IsEnabled "symbol" -}}
2018
export class Symbol {
21-
symbol: {{template "tokenType"}};
19+
symbol: {{template "tokenTypeRef" $}};
2220
offset: number;
2321
endoffset: number;
2422

25-
constructor(symbol: {{template "tokenType"}}, offset: number, endoffset: number) {
23+
constructor(symbol: {{template "tokenTypeRef" $}}, offset: number, endoffset: number) {
2624
this.symbol = symbol;
2725
this.offset = offset;
2826
this.endoffset = endoffset;

0 commit comments

Comments
 (0)