Skip to content

Commit cf7da4a

Browse files
committed
feat: support ts
1 parent 70c2e1c commit cf7da4a

4 files changed

Lines changed: 322 additions & 9 deletions

File tree

lang/parse.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"os"
2424
"os/exec"
2525
"path/filepath"
26+
"strings"
2627
"time"
2728

2829
"github.com/cloudwego/abcoder/lang/collect"
@@ -32,6 +33,7 @@ import (
3233
"github.com/cloudwego/abcoder/lang/lsp"
3334
"github.com/cloudwego/abcoder/lang/python"
3435
"github.com/cloudwego/abcoder/lang/rust"
36+
"github.com/cloudwego/abcoder/lang/typescript"
3537
"github.com/cloudwego/abcoder/lang/uniast"
3638
)
3739

@@ -44,7 +46,8 @@ type ParseOptions struct {
4446
collect.CollectOption
4547
// specify the repo id
4648
RepoID string
47-
49+
// 输出路径
50+
OutputPath string
4851
// TS options
4952
// tsconfig string
5053
TSParseOptions
@@ -61,6 +64,12 @@ func Parse(ctx context.Context, uri string, args ParseOptions) ([]byte, error) {
6164
if !filepath.IsAbs(uri) {
6265
uri, _ = filepath.Abs(uri)
6366
}
67+
68+
// Handle TypeScript separately
69+
if args.Language == uniast.TypeScript {
70+
return parseTSProject(ctx, uri, args)
71+
}
72+
6473
l, lspPath, err := checkLSP(args.Language, args.LSP)
6574
if err != nil {
6675
return nil, err
@@ -120,6 +129,8 @@ func checkRepoPath(repoPath string, language uniast.Language) (openfile string,
120129
openfile, wait = cxx.CheckRepo(repoPath)
121130
case uniast.Python:
122131
openfile, wait = python.CheckRepo(repoPath)
132+
case uniast.TypeScript:
133+
openfile, wait = typescript.CheckRepo(repoPath)
123134
default:
124135
openfile = ""
125136
wait = 0
@@ -137,6 +148,8 @@ func checkLSP(language uniast.Language, lspPath string) (l uniast.Language, s st
137148
l, s = cxx.GetDefaultLSP()
138149
case uniast.Python:
139150
l, s = python.GetDefaultLSP()
151+
case uniast.TypeScript:
152+
l, s = typescript.GetDefaultLSP()
140153
case uniast.Golang:
141154
l = uniast.Golang
142155
s = ""
@@ -214,3 +227,57 @@ func callGoParser(ctx context.Context, repoPath string, opts collect.CollectOpti
214227
}
215228
return &repo, nil
216229
}
230+
231+
func parseTSProject(ctx context.Context, repoPath string, opts ParseOptions) ([]byte, error) {
232+
parserPath, err := exec.LookPath("abcoder-ts-parser")
233+
if err != nil {
234+
log.Info("abcoder-ts-parser not found, installing...")
235+
cmd := exec.Command("npm", "install", "-g", "abcoder-ts-parser")
236+
cmd.Stdout = os.Stdout
237+
cmd.Stderr = os.Stderr
238+
if err := cmd.Run(); err != nil {
239+
return nil, fmt.Errorf("failed to install abcoder-ts-parser: %v", err)
240+
}
241+
parserPath, err = exec.LookPath("abcoder-ts-parser")
242+
if err != nil {
243+
return nil, fmt.Errorf("failed to find abcoder-ts-parser after installation: %v", err)
244+
}
245+
}
246+
247+
args := []string{"parse", repoPath}
248+
if len(opts.TSSrcDir) > 0 {
249+
args = append(args, "--src", strings.Join(opts.TSSrcDir, ","))
250+
}
251+
if opts.TSConfig != "" {
252+
args = append(args, "--tsconfig", opts.TSConfig)
253+
}
254+
255+
// Use a temporary file for output since we need to return the content
256+
tempFile, err := os.CreateTemp("", "abcoder-ts-*.json")
257+
if err != nil {
258+
return nil, fmt.Errorf("failed to create temp file: %v", err)
259+
}
260+
defer os.Remove(tempFile.Name())
261+
tempFile.Close()
262+
263+
args = append(args, "--output", tempFile.Name())
264+
265+
cmd := exec.CommandContext(ctx, parserPath, args...)
266+
cmd.Env = append(os.Environ(), "NODE_OPTIONS=--max-old-space-size=65536")
267+
var stderr strings.Builder
268+
cmd.Stderr = &stderr
269+
270+
log.Info("Running abcoder-ts-parser with args: %v", args)
271+
272+
if err := cmd.Run(); err != nil {
273+
return nil, fmt.Errorf("abcoder-ts-parser failed: %v, stderr: %s", err, stderr.String())
274+
}
275+
276+
// Read the output from the temp file
277+
output, err := os.ReadFile(tempFile.Name())
278+
if err != nil {
279+
return nil, fmt.Errorf("failed to read parser output: %v", err)
280+
}
281+
282+
return output, nil
283+
}

lang/typescript/lib.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2025 CloudWeGo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package typescript
16+
17+
import (
18+
"time"
19+
20+
"github.com/cloudwego/abcoder/lang/uniast"
21+
"github.com/cloudwego/abcoder/lang/utils"
22+
)
23+
24+
const MaxWaitDuration = 5 * time.Second
25+
26+
func GetDefaultLSP() (lang uniast.Language, name string) {
27+
return uniast.TypeScript, "typescript-language-server"
28+
}
29+
30+
func CheckRepo(repo string) (string, time.Duration) {
31+
openfile := ""
32+
33+
// Give the LSP sometime to initialize
34+
_, size := utils.CountFiles(repo, ".ts", "node_modules/")
35+
wait := 2*time.Second + time.Second*time.Duration(size/1024)
36+
if wait > MaxWaitDuration {
37+
wait = MaxWaitDuration
38+
}
39+
return openfile, wait
40+
}

lang/typescript/spec.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright 2025 CloudWeGo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package typescript
16+
17+
import (
18+
"fmt"
19+
"path/filepath"
20+
"strings"
21+
22+
lsp "github.com/cloudwego/abcoder/lang/lsp"
23+
"github.com/cloudwego/abcoder/lang/uniast"
24+
)
25+
26+
var _ lsp.LanguageSpec = (*TypeScriptSpec)(nil)
27+
28+
type TypeScriptSpec struct {
29+
repo string
30+
}
31+
32+
func NewTypeScriptSpec() *TypeScriptSpec {
33+
return &TypeScriptSpec{}
34+
}
35+
36+
func (c *TypeScriptSpec) FileImports(content []byte) ([]uniast.Import, error) {
37+
// TODO: Parse TypeScript import statements
38+
return []uniast.Import{}, nil
39+
}
40+
41+
func (c *TypeScriptSpec) IsExternalEntityToken(tok lsp.Token) bool {
42+
if !c.IsEntityToken(tok) {
43+
return false
44+
}
45+
for _, m := range tok.Modifiers {
46+
if m == "defaultLibrary" {
47+
return true
48+
}
49+
}
50+
return false
51+
}
52+
53+
func (c *TypeScriptSpec) TokenKind(tok lsp.Token) lsp.SymbolKind {
54+
switch tok.Type {
55+
case "class":
56+
return lsp.SKClass
57+
case "interface":
58+
return lsp.SKInterface
59+
case "function":
60+
return lsp.SKFunction
61+
case "method":
62+
return lsp.SKMethod
63+
case "property":
64+
return lsp.SKProperty
65+
case "variable":
66+
return lsp.SKVariable
67+
case "const":
68+
return lsp.SKConstant
69+
case "enum":
70+
return lsp.SKEnum
71+
case "enumMember":
72+
return lsp.SKEnumMember
73+
case "type":
74+
return lsp.SKTypeParameter
75+
case "namespace":
76+
return lsp.SKNamespace
77+
case "module":
78+
return lsp.SKModule
79+
default:
80+
return lsp.SKUnknown
81+
}
82+
}
83+
84+
func (c *TypeScriptSpec) IsStdToken(tok lsp.Token) bool {
85+
for _, m := range tok.Modifiers {
86+
if m == "defaultLibrary" {
87+
return true
88+
}
89+
}
90+
return false
91+
}
92+
93+
func (c *TypeScriptSpec) IsDocToken(tok lsp.Token) bool {
94+
for _, m := range tok.Modifiers {
95+
if m == "documentation" {
96+
return true
97+
}
98+
}
99+
return false
100+
}
101+
102+
func (c *TypeScriptSpec) DeclareTokenOfSymbol(sym lsp.DocumentSymbol) int {
103+
for i, t := range sym.Tokens {
104+
if c.IsDocToken(t) {
105+
continue
106+
}
107+
for _, m := range t.Modifiers {
108+
if m == "declaration" {
109+
return i
110+
}
111+
}
112+
}
113+
return -1
114+
}
115+
116+
func (c *TypeScriptSpec) IsPublicSymbol(sym lsp.DocumentSymbol) bool {
117+
// In TypeScript, symbols are public by default unless marked private/protected
118+
id := c.DeclareTokenOfSymbol(sym)
119+
if id == -1 {
120+
return true
121+
}
122+
for _, m := range sym.Tokens[id].Modifiers {
123+
if m == "private" || m == "protected" {
124+
return false
125+
}
126+
}
127+
return true
128+
}
129+
130+
func (c *TypeScriptSpec) IsMainFunction(sym lsp.DocumentSymbol) bool {
131+
// TypeScript doesn't have a main function concept
132+
return false
133+
}
134+
135+
func (c *TypeScriptSpec) IsEntitySymbol(sym lsp.DocumentSymbol) bool {
136+
typ := sym.Kind
137+
return typ == lsp.SKClass || typ == lsp.SKMethod || typ == lsp.SKFunction ||
138+
typ == lsp.SKVariable || typ == lsp.SKInterface || typ == lsp.SKConstant ||
139+
typ == lsp.SKEnum || typ == lsp.SKTypeParameter || typ == lsp.SKNamespace ||
140+
typ == lsp.SKModule
141+
}
142+
143+
func (c *TypeScriptSpec) IsEntityToken(tok lsp.Token) bool {
144+
typ := tok.Type
145+
return typ == "class" || typ == "interface" || typ == "function" ||
146+
typ == "method" || typ == "property" || typ == "variable" ||
147+
typ == "const" || typ == "enum" || typ == "enumMember" ||
148+
typ == "type" || typ == "namespace" || typ == "module"
149+
}
150+
151+
func (c *TypeScriptSpec) HasImplSymbol() bool {
152+
// TypeScript uses class/interface implementation, not impl blocks like Rust
153+
return false
154+
}
155+
156+
func (c *TypeScriptSpec) ImplSymbol(sym lsp.DocumentSymbol) (int, int, int) {
157+
// TypeScript doesn't have impl blocks
158+
return -1, -1, -1
159+
}
160+
161+
func (c *TypeScriptSpec) FunctionSymbol(sym lsp.DocumentSymbol) (int, []int, []int, []int) {
162+
// TODO: Implement TypeScript function parsing
163+
return -1, nil, nil, nil
164+
}
165+
166+
func (c *TypeScriptSpec) ShouldSkip(path string) bool {
167+
if strings.Contains(path, "/node_modules/") {
168+
return true
169+
}
170+
if !strings.HasSuffix(path, ".ts") && !strings.HasSuffix(path, ".tsx") {
171+
return true
172+
}
173+
return false
174+
}
175+
176+
func (c *TypeScriptSpec) NameSpace(path string) (string, string, error) {
177+
if !strings.HasPrefix(path, c.repo) {
178+
// External module
179+
return "", "", fmt.Errorf("external module: %s", path)
180+
}
181+
182+
// Calculate relative path from repo root
183+
rel, err := filepath.Rel(c.repo, path)
184+
if err != nil {
185+
return "", "", err
186+
}
187+
188+
// Remove file extension
189+
rel = strings.TrimSuffix(rel, ".ts")
190+
rel = strings.TrimSuffix(rel, ".tsx")
191+
192+
// Remove index suffix if present
193+
if strings.HasSuffix(rel, "/index") {
194+
rel = strings.TrimSuffix(rel, "/index")
195+
}
196+
197+
// Convert path to module name
198+
module := strings.ReplaceAll(rel, string(filepath.Separator), ".")
199+
200+
return module, module, nil
201+
}
202+
203+
func (c *TypeScriptSpec) WorkSpace(root string) (map[string]string, error) {
204+
c.repo = root
205+
// For TypeScript, we don't need to collect modules like Rust
206+
// The module system is based on file paths
207+
return map[string]string{}, nil
208+
}
209+
210+
func (c *TypeScriptSpec) GetUnloadedSymbol(from lsp.Token, loc lsp.Location) (string, error) {
211+
// TODO: Implement TypeScript unloaded symbol extraction
212+
return "", nil
213+
}

main.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func main() {
8080
flags.StringVar(&opts.RepoID, "repo-id", "", "specify the repo id")
8181
flags.StringVar(&opts.TSConfig, "tsconfig", "", "tsconfig path (only works for TS now)")
8282
flags.Var((*StringArray)(&opts.TSSrcDir), "ts-src-dir", "src-dir path (only works for TS now)")
83+
opts.OutputPath = *flagOutput
8384

8485
var wopts lang.WriteOptions
8586
flags.StringVar(&wopts.Compiler, "compiler", "", "destination compiler path.")
@@ -114,14 +115,6 @@ func main() {
114115

115116
opts.Language = language
116117

117-
if language == uniast.TypeScript {
118-
if err := parseTSProject(context.Background(), uri, opts, flagOutput); err != nil {
119-
log.Error("Failed to parse: %v\n", err)
120-
os.Exit(1)
121-
}
122-
return
123-
}
124-
125118
if flagLsp != nil {
126119
opts.LSP = *flagLsp
127120
}

0 commit comments

Comments
 (0)