From cf7da4a5db66fd0bb4670d359b909b4a5d46e131 Mon Sep 17 00:00:00 2001 From: "yinxuran.lucky" Date: Wed, 10 Sep 2025 19:56:30 +0800 Subject: [PATCH] feat: support ts --- lang/parse.go | 69 ++++++++++++- lang/typescript/lib.go | 40 ++++++++ lang/typescript/spec.go | 213 ++++++++++++++++++++++++++++++++++++++++ main.go | 9 +- 4 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 lang/typescript/lib.go create mode 100644 lang/typescript/spec.go diff --git a/lang/parse.go b/lang/parse.go index 0afa5be..f14725e 100644 --- a/lang/parse.go +++ b/lang/parse.go @@ -23,6 +23,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "time" "github.com/cloudwego/abcoder/lang/collect" @@ -32,6 +33,7 @@ import ( "github.com/cloudwego/abcoder/lang/lsp" "github.com/cloudwego/abcoder/lang/python" "github.com/cloudwego/abcoder/lang/rust" + "github.com/cloudwego/abcoder/lang/typescript" "github.com/cloudwego/abcoder/lang/uniast" ) @@ -44,7 +46,8 @@ type ParseOptions struct { collect.CollectOption // specify the repo id RepoID string - + // 输出路径 + OutputPath string // TS options // tsconfig string TSParseOptions @@ -61,6 +64,12 @@ func Parse(ctx context.Context, uri string, args ParseOptions) ([]byte, error) { if !filepath.IsAbs(uri) { uri, _ = filepath.Abs(uri) } + + // Handle TypeScript separately + if args.Language == uniast.TypeScript { + return parseTSProject(ctx, uri, args) + } + l, lspPath, err := checkLSP(args.Language, args.LSP) if err != nil { return nil, err @@ -120,6 +129,8 @@ func checkRepoPath(repoPath string, language uniast.Language) (openfile string, openfile, wait = cxx.CheckRepo(repoPath) case uniast.Python: openfile, wait = python.CheckRepo(repoPath) + case uniast.TypeScript: + openfile, wait = typescript.CheckRepo(repoPath) default: openfile = "" wait = 0 @@ -137,6 +148,8 @@ func checkLSP(language uniast.Language, lspPath string) (l uniast.Language, s st l, s = cxx.GetDefaultLSP() case uniast.Python: l, s = python.GetDefaultLSP() + case uniast.TypeScript: + l, s = typescript.GetDefaultLSP() case uniast.Golang: l = uniast.Golang s = "" @@ -214,3 +227,57 @@ func callGoParser(ctx context.Context, repoPath string, opts collect.CollectOpti } return &repo, nil } + +func parseTSProject(ctx context.Context, repoPath string, opts ParseOptions) ([]byte, error) { + parserPath, err := exec.LookPath("abcoder-ts-parser") + if err != nil { + log.Info("abcoder-ts-parser not found, installing...") + cmd := exec.Command("npm", "install", "-g", "abcoder-ts-parser") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("failed to install abcoder-ts-parser: %v", err) + } + parserPath, err = exec.LookPath("abcoder-ts-parser") + if err != nil { + return nil, fmt.Errorf("failed to find abcoder-ts-parser after installation: %v", err) + } + } + + args := []string{"parse", repoPath} + if len(opts.TSSrcDir) > 0 { + args = append(args, "--src", strings.Join(opts.TSSrcDir, ",")) + } + if opts.TSConfig != "" { + args = append(args, "--tsconfig", opts.TSConfig) + } + + // Use a temporary file for output since we need to return the content + tempFile, err := os.CreateTemp("", "abcoder-ts-*.json") + if err != nil { + return nil, fmt.Errorf("failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + args = append(args, "--output", tempFile.Name()) + + cmd := exec.CommandContext(ctx, parserPath, args...) + cmd.Env = append(os.Environ(), "NODE_OPTIONS=--max-old-space-size=65536") + var stderr strings.Builder + cmd.Stderr = &stderr + + log.Info("Running abcoder-ts-parser with args: %v", args) + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("abcoder-ts-parser failed: %v, stderr: %s", err, stderr.String()) + } + + // Read the output from the temp file + output, err := os.ReadFile(tempFile.Name()) + if err != nil { + return nil, fmt.Errorf("failed to read parser output: %v", err) + } + + return output, nil +} diff --git a/lang/typescript/lib.go b/lang/typescript/lib.go new file mode 100644 index 0000000..561dc7c --- /dev/null +++ b/lang/typescript/lib.go @@ -0,0 +1,40 @@ +// Copyright 2025 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package typescript + +import ( + "time" + + "github.com/cloudwego/abcoder/lang/uniast" + "github.com/cloudwego/abcoder/lang/utils" +) + +const MaxWaitDuration = 5 * time.Second + +func GetDefaultLSP() (lang uniast.Language, name string) { + return uniast.TypeScript, "typescript-language-server" +} + +func CheckRepo(repo string) (string, time.Duration) { + openfile := "" + + // Give the LSP sometime to initialize + _, size := utils.CountFiles(repo, ".ts", "node_modules/") + wait := 2*time.Second + time.Second*time.Duration(size/1024) + if wait > MaxWaitDuration { + wait = MaxWaitDuration + } + return openfile, wait +} \ No newline at end of file diff --git a/lang/typescript/spec.go b/lang/typescript/spec.go new file mode 100644 index 0000000..0736484 --- /dev/null +++ b/lang/typescript/spec.go @@ -0,0 +1,213 @@ +// Copyright 2025 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package typescript + +import ( + "fmt" + "path/filepath" + "strings" + + lsp "github.com/cloudwego/abcoder/lang/lsp" + "github.com/cloudwego/abcoder/lang/uniast" +) + +var _ lsp.LanguageSpec = (*TypeScriptSpec)(nil) + +type TypeScriptSpec struct { + repo string +} + +func NewTypeScriptSpec() *TypeScriptSpec { + return &TypeScriptSpec{} +} + +func (c *TypeScriptSpec) FileImports(content []byte) ([]uniast.Import, error) { + // TODO: Parse TypeScript import statements + return []uniast.Import{}, nil +} + +func (c *TypeScriptSpec) IsExternalEntityToken(tok lsp.Token) bool { + if !c.IsEntityToken(tok) { + return false + } + for _, m := range tok.Modifiers { + if m == "defaultLibrary" { + return true + } + } + return false +} + +func (c *TypeScriptSpec) TokenKind(tok lsp.Token) lsp.SymbolKind { + switch tok.Type { + case "class": + return lsp.SKClass + case "interface": + return lsp.SKInterface + case "function": + return lsp.SKFunction + case "method": + return lsp.SKMethod + case "property": + return lsp.SKProperty + case "variable": + return lsp.SKVariable + case "const": + return lsp.SKConstant + case "enum": + return lsp.SKEnum + case "enumMember": + return lsp.SKEnumMember + case "type": + return lsp.SKTypeParameter + case "namespace": + return lsp.SKNamespace + case "module": + return lsp.SKModule + default: + return lsp.SKUnknown + } +} + +func (c *TypeScriptSpec) IsStdToken(tok lsp.Token) bool { + for _, m := range tok.Modifiers { + if m == "defaultLibrary" { + return true + } + } + return false +} + +func (c *TypeScriptSpec) IsDocToken(tok lsp.Token) bool { + for _, m := range tok.Modifiers { + if m == "documentation" { + return true + } + } + return false +} + +func (c *TypeScriptSpec) DeclareTokenOfSymbol(sym lsp.DocumentSymbol) int { + for i, t := range sym.Tokens { + if c.IsDocToken(t) { + continue + } + for _, m := range t.Modifiers { + if m == "declaration" { + return i + } + } + } + return -1 +} + +func (c *TypeScriptSpec) IsPublicSymbol(sym lsp.DocumentSymbol) bool { + // In TypeScript, symbols are public by default unless marked private/protected + id := c.DeclareTokenOfSymbol(sym) + if id == -1 { + return true + } + for _, m := range sym.Tokens[id].Modifiers { + if m == "private" || m == "protected" { + return false + } + } + return true +} + +func (c *TypeScriptSpec) IsMainFunction(sym lsp.DocumentSymbol) bool { + // TypeScript doesn't have a main function concept + return false +} + +func (c *TypeScriptSpec) IsEntitySymbol(sym lsp.DocumentSymbol) bool { + typ := sym.Kind + return typ == lsp.SKClass || typ == lsp.SKMethod || typ == lsp.SKFunction || + typ == lsp.SKVariable || typ == lsp.SKInterface || typ == lsp.SKConstant || + typ == lsp.SKEnum || typ == lsp.SKTypeParameter || typ == lsp.SKNamespace || + typ == lsp.SKModule +} + +func (c *TypeScriptSpec) IsEntityToken(tok lsp.Token) bool { + typ := tok.Type + return typ == "class" || typ == "interface" || typ == "function" || + typ == "method" || typ == "property" || typ == "variable" || + typ == "const" || typ == "enum" || typ == "enumMember" || + typ == "type" || typ == "namespace" || typ == "module" +} + +func (c *TypeScriptSpec) HasImplSymbol() bool { + // TypeScript uses class/interface implementation, not impl blocks like Rust + return false +} + +func (c *TypeScriptSpec) ImplSymbol(sym lsp.DocumentSymbol) (int, int, int) { + // TypeScript doesn't have impl blocks + return -1, -1, -1 +} + +func (c *TypeScriptSpec) FunctionSymbol(sym lsp.DocumentSymbol) (int, []int, []int, []int) { + // TODO: Implement TypeScript function parsing + return -1, nil, nil, nil +} + +func (c *TypeScriptSpec) ShouldSkip(path string) bool { + if strings.Contains(path, "/node_modules/") { + return true + } + if !strings.HasSuffix(path, ".ts") && !strings.HasSuffix(path, ".tsx") { + return true + } + return false +} + +func (c *TypeScriptSpec) NameSpace(path string) (string, string, error) { + if !strings.HasPrefix(path, c.repo) { + // External module + return "", "", fmt.Errorf("external module: %s", path) + } + + // Calculate relative path from repo root + rel, err := filepath.Rel(c.repo, path) + if err != nil { + return "", "", err + } + + // Remove file extension + rel = strings.TrimSuffix(rel, ".ts") + rel = strings.TrimSuffix(rel, ".tsx") + + // Remove index suffix if present + if strings.HasSuffix(rel, "/index") { + rel = strings.TrimSuffix(rel, "/index") + } + + // Convert path to module name + module := strings.ReplaceAll(rel, string(filepath.Separator), ".") + + return module, module, nil +} + +func (c *TypeScriptSpec) WorkSpace(root string) (map[string]string, error) { + c.repo = root + // For TypeScript, we don't need to collect modules like Rust + // The module system is based on file paths + return map[string]string{}, nil +} + +func (c *TypeScriptSpec) GetUnloadedSymbol(from lsp.Token, loc lsp.Location) (string, error) { + // TODO: Implement TypeScript unloaded symbol extraction + return "", nil +} \ No newline at end of file diff --git a/main.go b/main.go index 740c706..ad77557 100644 --- a/main.go +++ b/main.go @@ -80,6 +80,7 @@ func main() { flags.StringVar(&opts.RepoID, "repo-id", "", "specify the repo id") flags.StringVar(&opts.TSConfig, "tsconfig", "", "tsconfig path (only works for TS now)") flags.Var((*StringArray)(&opts.TSSrcDir), "ts-src-dir", "src-dir path (only works for TS now)") + opts.OutputPath = *flagOutput var wopts lang.WriteOptions flags.StringVar(&wopts.Compiler, "compiler", "", "destination compiler path.") @@ -114,14 +115,6 @@ func main() { opts.Language = language - if language == uniast.TypeScript { - if err := parseTSProject(context.Background(), uri, opts, flagOutput); err != nil { - log.Error("Failed to parse: %v\n", err) - os.Exit(1) - } - return - } - if flagLsp != nil { opts.LSP = *flagLsp }