From 8db4d7ae1f65d8c374e740af2102713f3e73446a Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Tue, 13 Aug 2024 06:54:38 +0100 Subject: [PATCH 1/5] Add TS support This is a POC for adding TypeScript support to Puku. It uses [golang-tree-sitter](https://github.com/smacker/go-tree-sitter) to parse the TS files. --- .plzconfig | 6 ++ config/BUILD | 6 +- config/config.go | 54 ++++++++++ generate/BUILD | 6 +- generate/deps.go | 198 ++++++++++++++++++++++++++++++++++ generate/generate.go | 84 ++++++++++----- generate/generate_test.go | 105 ++++++++++-------- generate/import.go | 219 ++++++++++++++++++++++++++++++++------ generate/import_test.go | 35 +++++- go.mod | 2 + go.sum | 5 + kinds/kinds.go | 5 + logging/BUILD | 1 + plugins/BUILD | 2 +- test_project/ts/foo.ts | 1 + third_party/go/BUILD | 12 +++ 16 files changed, 629 insertions(+), 112 deletions(-) create mode 100644 test_project/ts/foo.ts diff --git a/.plzconfig b/.plzconfig index 395bc2c..c55dd1a 100644 --- a/.plzconfig +++ b/.plzconfig @@ -8,6 +8,12 @@ GoTool = //third_party/go:toolchain|go ModFile = //:mod RequireLicences = true Stdlib = //third_party/go:std +CgoEnabled = true + +[Plugin "cc"] +target = //plugins:cc +defaultoptcppflags = --std=c++11 -O2 -DNDEBUG -Wall -Wextra -Werror -Wno-unused-parameter +defaultdbgcppflags = --std=c++11 -g3 -DDEBUG -Wall -Wextra -Werror -Wno-unused-parameter [Alias "puku"] Cmd = run //cmd/puku -- diff --git a/config/BUILD b/config/BUILD index 67603ae..d481af4 100644 --- a/config/BUILD +++ b/config/BUILD @@ -13,7 +13,11 @@ go_library( "//sync/integration/syncmod:all", "//work:all", ], - deps = ["//kinds"], + deps = [ + "///third_party/go/github.com_muhammadmuzzammil1998_jsonc//:jsonc", + "//kinds", + "//logging", + ], ) go_test( diff --git a/config/config.go b/config/config.go index 08c6d69..68d3322 100644 --- a/config/config.go +++ b/config/config.go @@ -7,9 +7,13 @@ import ( "path/filepath" "strings" + "github.com/muhammadmuzzammil1998/jsonc" "github.com/please-build/puku/kinds" + "github.com/please-build/puku/logging" ) +var log = logging.GetLogger() + // KindConfig represents the configuration for a custom kind. See kinds.Kind for more information on how kinds work. type KindConfig struct { // NonGoSources indicates that this rule operates on non-go sources and we shouldn't attempt to parse them to @@ -209,3 +213,53 @@ func (c *Config) GetKind(kind string) *kinds.Kind { } return nil } + +// TSConfig represents a tsconfig.json file discovered in the repo. +type TSConfig struct { + Dir string + CompilerOptions struct { + Paths map[string][]string `json:"paths"` + } `json:"compilerOptions"` +} + +var tsconfigs = map[string]*TSConfig{} + +// ReadTSConfig finds the closest tsconfig by walking up the directory tree. +// Note: we don't try and resolve the full config inheritance, it just resolves +// to the first tsconfig.json file that we find. +func ReadTSConfig(dir string) (*TSConfig, error) { + origDir := dir + dir = filepath.Clean(dir) + + for true { + if c, ok := tsconfigs[dir]; ok { + return c, nil + } + + f, err := os.ReadFile(filepath.Join(dir, "tsconfig.json")) + if err != nil { + if os.IsNotExist(err) { + if dir == "." { + break + } + + // try parent directory + dir = filepath.Dir(dir) + continue + } + return nil, err + } + + c := new(TSConfig) + c.Dir = dir + // tsconfig files are jsonc: https://code.visualstudio.com/docs/languages/json#_json-with-comments + if err := jsonc.Unmarshal(f, c); err != nil { + return nil, fmt.Errorf("in %s: %w", dir, err) + } + tsconfigs[dir] = c + return c, nil + } + + log.Debugf("Can't find tsconfig for dir: %s", origDir) + return nil, nil +} diff --git a/generate/BUILD b/generate/BUILD index 8202056..69e0616 100644 --- a/generate/BUILD +++ b/generate/BUILD @@ -16,6 +16,8 @@ go_library( deps = [ "///third_party/go/github.com_please-build_buildtools//build", "///third_party/go/github.com_please-build_buildtools//labels", + "///third_party/go/github.com_smacker_go-tree-sitter//:go-tree-sitter", + "///third_party/go/github.com_smacker_go-tree-sitter//typescript/typescript", "//config", "//edit", "//eval", @@ -26,10 +28,10 @@ go_library( "//knownimports", "//licences", "//logging", + "//options", "//please", "//proxy", "//trie", - "//options", ], ) @@ -44,9 +46,9 @@ testify_test( "//config", "//edit", "//kinds", + "//options", "//please", "//proxy", "//trie", - "//options", ], ) diff --git a/generate/deps.go b/generate/deps.go index 964a497..0e3222c 100644 --- a/generate/deps.go +++ b/generate/deps.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "github.com/please-build/buildtools/build" @@ -101,6 +102,127 @@ func (u *updater) reallyResolveImport(conf *config.Config, i string) (string, er return "", fmt.Errorf("module not found") } +// resolveTSImport resolves an import path to a build target. It will return an +// empty string if the import is for a third party package. Otherwise, it will +// return the build target for that dependency, or an error if it can't be resolved. +func (u *updater) resolveTSImport(conf *config.Config, tsConfig *config.TSConfig, f *SourceFile, importPath string, currentRule *edit.Rule) (string, error) { + var t string + var err error + + var importPaths []string + type tsAlias struct { + alias string + targets []string + } + var matchedAlias *tsAlias + + tsPaths := make(map[string][]string) + if tsConfig != nil { + tsPaths = tsConfig.CompilerOptions.Paths + } + + for alias, targets := range tsPaths { + re := regexp.MustCompile(wildCardToRegexp(alias)) + matched := re.FindString(importPath) + if err != nil { + return t, err + } + + if matched != "" { + matchedAlias = &tsAlias{ + alias: alias, + targets: targets, + } + } + } + + // Ignore all imports that aren't relative + if !strings.HasPrefix(importPath, ".") && matchedAlias == nil { + log.Debugf("Skipping TS import (not relative): %s", importPath) + return t, nil + } + + if matchedAlias != nil { + // TODO: at the moment we only support the first target and we only support + // aliases that are simple prefix replacements + target := matchedAlias.targets[0] + alias := matchedAlias.alias + origImportPath := importPath + + aliasPrefix := alias[0:strings.Index(alias, "*")] + targetPrefix := target[0:strings.Index(target, "*")] + + importPath = strings.Replace(importPath, aliasPrefix, targetPrefix, 1) + importPath = filepath.Join(tsConfig.Dir, importPath) + log.Debugf("alias matched %s %s; newImportPath: %s", alias, origImportPath, importPath) + } + + // If importPath is a folder then append `index.{ts,tsx}` + if filepath.Ext(importPath) == "" { + // filepath.Join removes the './' at the beginning of the import so we can't + // use it + importPaths = append(importPaths, importPath+".ts") + importPaths = append(importPaths, importPath+".tsx") + importPaths = append(importPaths, importPath+string(filepath.Separator)+"index.ts") + importPaths = append(importPaths, importPath+string(filepath.Separator)+"index.tsx") + log.Debugf("adding file extensions and index paths: %s", importPaths) + } else { + importPaths = append(importPaths, importPath) + } + + // Try every possible import path + for _, path := range importPaths { + fullPath := path + if strings.HasPrefix(path, ".") { + fullPath = filepath.Join(f.Dir(), path) + } + log.Debugf("fullPath: %s", fullPath) + + if t, ok := u.resolvedImports[fullPath]; ok { + return t, nil + } + + // TODO + if t := conf.GetKnownTarget(fullPath); t != "" { + return t, nil + } + + // Check to see if the target exists in the current repo + t, err = u.localTSDep(fullPath, currentRule) + if err != nil { + return "", err + } + + if t != "" { + u.resolvedImports[fullPath] = t + return t, nil + } + } + + return "", nil +} + +func wildCardToRegexp(pattern string) string { + components := strings.Split(pattern, "*") + if len(components) == 1 { + // if len is 1, there are no *'s, return exact match pattern + return "^" + pattern + "$" + } + var result strings.Builder + for i, literal := range components { + + // Replace * with .* + if i > 0 { + result.WriteString("(.*)") + } + + // Quote any regular expression meta characters in the + // literal text. + result.WriteString(regexp.QuoteMeta(literal)) + } + return "^" + result.String() + "$" +} + // isInScope returns true when the given path is in scope of the current run i.e. if we are going to format the BUILD // file there. func (u *updater) isInScope(path string) bool { @@ -170,6 +292,82 @@ func (u *updater) localDep(importPath string) (string, error) { return "", nil } +// localTSDep finds a dependency local to this repository, checking the BUILD +// file for a js_library target. Returns an empty string when no target is found. +func (u *updater) localTSDep(importPath string, currentRule *edit.Rule) (string, error) { + path := filepath.Dir(importPath) + // Check the directory exists. If it doesn't it's not a local import. + if _, err := os.Lstat(path); os.IsNotExist(err) { + log.Debugf("dir doesn't exist %s", path) + return "", nil + } + file, err := u.graph.LoadFile(path) + if err != nil { + return "", fmt.Errorf("failed to parse BUILD files in %v: %v", path, err) + } + + conf, err := config.ReadConfig(path) + if err != nil { + return "", err + } + + // TODO allow other rule names? + for _, rule := range file.Rules("js_library") { + kind := conf.GetKind(rule.Kind()) + if kind == nil { + continue + } + + // Skip rules that are the same as the current rule to prevent circular + // imports + if currentRule.Dir == path && rule.Name() == currentRule.Name() { + continue + } + + if kind.Type == kinds.Lib { + ruleSrcs, err := u.eval.EvalGlobs(path, rule, kind.SrcsAttr) + if err != nil { + return "", err + } + + // TODO if rule is the same as the current rule then skip + + // Check if import file matches any of the srcs + for _, src := range ruleSrcs { + fileName := filepath.Base(importPath) + // Files don't have to have an extension. If they don't then they could + // map with .ts or .tsx files. + if src == fileName || src == fileName+".ts" || src == fileName+".tsx" { + log.Debugf("found rule for import %s: %s:%s", importPath, path, rule.Name()) + return edit.BuildTarget(rule.Name(), path, ""), nil + } + } + } + } + + // if !u.isInScope(importPath) { + // return "", fmt.Errorf("resolved %v to a local package, but no library target was found and it's not in scope to generate the target", importPath) + // } + + // files, err := ImportDir(path) + // if err != nil { + // if os.IsNotExist(err) { + // return "", nil + // } + // return "", fmt.Errorf("failed to import %v: %v", path, err) + // } + + // If there are any non-test sources, then we will generate a js_library here later on. Return that target name. + // for _, f := range files { + // if !f.IsTest() { + // return BuildTarget(filepath.Base(importPath), path, ""), nil + // } + // } + + log.Debugf("failed to find rule for import: %s", importPath) + return "", nil +} + func depTarget(modules []string, importPath, thirdPartyFolder string) string { module := moduleForPackage(modules, importPath) if module == "" { diff --git a/generate/generate.go b/generate/generate.go index 8ad5a7d..b706b0f 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -255,13 +255,13 @@ func (u *updater) addNewModules(conf *config.Config) error { // goFiles contains a mapping of source files to their GoFile. This map might be missing entries from passedSources, if // the source doesn't actually exist. In which case, this should be removed from the rule, as the user likely deleted // the file. -func (u *updater) allSources(conf *config.Config, r *edit.Rule, sourceMap map[string]*GoFile) (passedSources []string, goFiles map[string]*GoFile, err error) { +func (u *updater) allSources(conf *config.Config, r *edit.Rule, sourceMap map[string]*SourceFile) (passedSources []string, goFiles map[string]*SourceFile, err error) { srcs, err := u.eval.BuildSources(conf.GetPlzPath(), r.Dir, r.Rule, r.SrcsAttr()) if err != nil { return nil, nil, err } - sources := make(map[string]*GoFile, len(srcs)) + sources := make(map[string]*SourceFile, len(srcs)) for _, src := range srcs { if file, ok := sourceMap[src]; ok { sources[src] = file @@ -269,7 +269,7 @@ func (u *updater) allSources(conf *config.Config, r *edit.Rule, sourceMap map[st } // These are generated sources in plz-out/gen - f, err := importFile(".", src) + f, err := importGoFile(".", src) if err != nil { continue } @@ -301,7 +301,7 @@ func isExternal(rule *edit.Rule) bool { } // updateRuleDeps updates the dependencies of a build rule based on the imports of its sources -func (u *updater) updateRuleDeps(conf *config.Config, rule *edit.Rule, rules []*edit.Rule, packageFiles map[string]*GoFile) error { +func (u *updater) updateRuleDeps(conf *config.Config, rule *edit.Rule, rules []*edit.Rule, packageFiles map[string]*SourceFile) error { done := map[string]struct{}{} // If the rule operates on non-go source files (e.g. *.proto for proto_library) then we should skip updating @@ -324,7 +324,16 @@ func (u *updater) updateRuleDeps(conf *config.Config, rule *edit.Rule, rules []* rule.RemoveSrc(src) // The src doesn't exist so remove it from the list of srcs continue } - for _, i := range f.Imports { + + var tsConfig *config.TSConfig + if f.fileType == TS { + tsConfig, err = config.ReadTSConfig(f.Dir()) + if err != nil { + log.Warningf("error reading tsconfig for %s: %v", f.FileName(), err) + } + } + + for _, i := range f.Imports() { if _, ok := done[i]; ok { continue } @@ -332,10 +341,20 @@ func (u *updater) updateRuleDeps(conf *config.Config, rule *edit.Rule, rules []* // If the dep is provided by the kind (i.e. the build def adds it) then skip this import - dep, err := u.resolveImport(conf, i) - if err != nil { - log.Warningf("couldn't resolve %q for %v: %v", i, rule.Label(), err) - continue + var dep string + if f.fileType == TS { + log.Debugf("Resolving TS file: %s (dir: %s)", i, f.Dir()) + dep, err = u.resolveTSImport(conf, tsConfig, f, i, rule) + if err != nil { + log.Warningf("couldn't resolve %q for %v: %v", i, rule.Label(), err) + continue + } + } else { + dep, err = u.resolveImport(conf, i) + if err != nil { + log.Warningf("couldn't resolve %q for %v: %v", i, rule.Label(), err) + continue + } } if dep == "" { continue @@ -419,7 +438,7 @@ func (u *updater) readRulesFromFile(conf *config.Config, file *build.File, pkgDi } // updateDeps updates the existing rules and creates any new rules in the BUILD file -func (u *updater) updateDeps(conf *config.Config, file *build.File, ruleExprs map[string]*build.Rule, rules []*edit.Rule, sources map[string]*GoFile) error { +func (u *updater) updateDeps(conf *config.Config, file *build.File, ruleExprs map[string]*build.Rule, rules []*edit.Rule, sources map[string]*SourceFile) error { for _, rule := range rules { if _, ok := ruleExprs[rule.Name()]; !ok { file.Stmt = append(file.Stmt, rule.Call) @@ -433,7 +452,7 @@ func (u *updater) updateDeps(conf *config.Config, file *build.File, ruleExprs ma // allocateSources allocates sources to rules. If there's no existing rule, a new rule will be created and returned // from this function -func (u *updater) allocateSources(conf *config.Config, pkgDir string, sources map[string]*GoFile, rules []*edit.Rule) ([]*edit.Rule, error) { +func (u *updater) allocateSources(conf *config.Config, pkgDir string, sources map[string]*SourceFile, rules []*edit.Rule) ([]*edit.Rule, error) { unallocated, err := u.unallocatedSources(sources, rules) if err != nil { return nil, err @@ -447,7 +466,7 @@ func (u *updater) allocateSources(conf *config.Config, pkgDir string, sources ma } var rule *edit.Rule for _, r := range append(rules, newRules...) { - if r.Kind.Type != importedFile.kindType() { + if r.Kind.Type != importedFile.KindType() { continue } @@ -459,21 +478,35 @@ func (u *updater) allocateSources(conf *config.Config, pkgDir string, sources ma // Find a rule that's for the same package and of the same kind (i.e. bin, lib, test) // NB: we return when we find the first one so if there are multiple options, we will pick one essentially at // random. - if rulePkgName == "" || rulePkgName == importedFile.Name { + if rulePkgName == "" || rulePkgName == importedFile.Name() { rule = r break } } if rule == nil { - name := filepath.Base(pkgDir) - kind := "go_library" - if importedFile.IsTest() { - name += "_test" - kind = "go_test" - } - if importedFile.IsCmd() { - kind = "go_binary" - name = "main" + var kind, name string + + // Handle TS files + if importedFile.fileType == TS { + kind = "js_library" + name = importedFile.Name() + if importedFile.IsTest() { + // skip tests for now + continue + // name += "_test" + // kind = "js_test" // TODO + } + } else { + // Default to assuming we're dealing with a go file + kind = "go_library" + if importedFile.IsTest() { + name += "_test" + kind = "go_test" + } + if importedFile.IsCmd() { + kind = "go_binary" + name = "main" + } } rule = edit.NewRule(edit.NewRuleExpr(kind, name), kinds.DefaultKinds[kind], pkgDir) if importedFile.IsExternal(filepath.Join(u.plzConf.ImportPath(), pkgDir)) { @@ -489,8 +522,9 @@ func (u *updater) allocateSources(conf *config.Config, pkgDir string, sources ma // rulePkg checks the first source it finds for a rule and returns the name from the "package name" directive at the top // of the file -func (u *updater) rulePkg(conf *config.Config, srcs map[string]*GoFile, rule *edit.Rule) (string, error) { +func (u *updater) rulePkg(conf *config.Config, srcs map[string]*SourceFile, rule *edit.Rule) (string, error) { // This is a safe bet if we can't use the source files to figure this out. + // TODO rename this since we now have more than 1 type of file. if rule.Kind.NonGoSources { return rule.Name(), nil } @@ -502,7 +536,7 @@ func (u *updater) rulePkg(conf *config.Config, srcs map[string]*GoFile, rule *ed for _, s := range ss { if src, ok := srcs[s]; ok { - return src.Name, nil + return src.Name(), nil } } @@ -510,7 +544,7 @@ func (u *updater) rulePkg(conf *config.Config, srcs map[string]*GoFile, rule *ed } // unallocatedSources returns all the sources that don't already belong to a rule -func (u *updater) unallocatedSources(srcs map[string]*GoFile, rules []*edit.Rule) ([]string, error) { +func (u *updater) unallocatedSources(srcs map[string]*SourceFile, rules []*edit.Rule) ([]string, error) { var ret []string for src := range srcs { found := false diff --git a/generate/generate_test.go b/generate/generate_test.go index 9dcb924..a6cd140 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -22,26 +22,31 @@ func TestAllocateSources(t *testing.T) { rules := []*edit.Rule{foo, fooTest} - files := map[string]*GoFile{ + files := map[string]*SourceFile{ "bar.go": { - Name: "foo", - FileName: "bar.go", + name: "foo", + fileName: "bar.go", + fileType: GO, }, "bar_test.go": { - Name: "foo", - FileName: "bar_test.go", + name: "foo", + fileName: "bar_test.go", + fileType: GO, }, "external_test.go": { - Name: "foo_test", - FileName: "external_test.go", + name: "foo_test", + fileName: "external_test.go", + fileType: GO, }, "foo.go": { - Name: "foo", - FileName: "foo.go", + name: "foo", + fileName: "foo.go", + fileType: GO, }, "foo_test.go": { - Name: "foo", - FileName: "foo_test.go", + name: "foo", + fileName: "foo_test.go", + fileType: GO, }, } @@ -62,14 +67,16 @@ func TestAddingLibDepToTest(t *testing.T) { foo := edit.NewRule(edit.NewRuleExpr("go_library", "foo"), kinds.DefaultKinds["go_library"], "") fooTest := edit.NewRule(edit.NewRuleExpr("go_test", "foo_test"), kinds.DefaultKinds["go_test"], "") - files := map[string]*GoFile{ + files := map[string]*SourceFile{ "foo.go": { - Name: "foo", - FileName: "foo.go", + name: "foo", + fileName: "foo.go", + fileType: GO, }, "foo_test.go": { - Name: "foo", - FileName: "foo_test.go", + name: "foo", + fileName: "foo_test.go", + fileType: GO, }, } @@ -104,22 +111,26 @@ func TestAllocateSourcesToCustomKind(t *testing.T) { rules := []*edit.Rule{foo, fooTest} - files := map[string]*GoFile{ + files := map[string]*SourceFile{ "bar.go": { - Name: "foo", - FileName: "bar.go", + name: "foo", + fileName: "bar.go", + fileType: GO, }, "bar_test.go": { - Name: "foo", - FileName: "bar_test.go", + name: "foo", + fileName: "bar_test.go", + fileType: GO, }, "foo.go": { - Name: "foo", - FileName: "foo.go", + name: "foo", + fileName: "foo.go", + fileType: GO, }, "foo_test.go": { - Name: "foo", - FileName: "foo_test.go", + name: "foo", + fileName: "foo_test.go", + fileType: GO, }, } @@ -146,10 +157,11 @@ func TestAllocateSourcesToNonGoKind(t *testing.T) { rules := []*edit.Rule{foo} - files := map[string]*GoFile{ + files := map[string]*SourceFile{ "foo.go": { - Name: "foo", - FileName: "foo.go", + name: "foo", + fileName: "foo.go", + fileType: GO, }, } @@ -173,7 +185,7 @@ func TestUpdateDeps(t *testing.T) { testCases := []struct { name string - srcs []*GoFile + srcs []*SourceFile rule *ruleKind expectedDeps []string modules []string @@ -183,11 +195,12 @@ func TestUpdateDeps(t *testing.T) { }{ { name: "adds import from known module", - srcs: []*GoFile{ + srcs: []*SourceFile{ { - FileName: "foo.go", - Imports: []string{"github.com/example/module/foo"}, - Name: "foo", + fileName: "foo.go", + imports: []string{"github.com/example/module/foo"}, + name: "foo", + fileType: GO, }, }, modules: []string{"github.com/example/module"}, @@ -199,14 +212,15 @@ func TestUpdateDeps(t *testing.T) { }, { name: "handles installs", - srcs: []*GoFile{ + srcs: []*SourceFile{ { - FileName: "foo.go", - Imports: []string{ + fileName: "foo.go", + imports: []string{ "github.com/example/module1/foo", "github.com/example/module2/foo/bar/baz", }, - Name: "foo", + name: "foo", + fileType: GO, }, }, modules: []string{}, @@ -223,14 +237,15 @@ func TestUpdateDeps(t *testing.T) { }, { name: "handles custom kinds", - srcs: []*GoFile{ + srcs: []*SourceFile{ { - FileName: "foo.go", - Imports: []string{ + fileName: "foo.go", + imports: []string{ "github.com/example/module/foo", "github.com/example/module/bar", }, - Name: "foo", + name: "foo", + fileType: GO, }, }, modules: []string{"github.com/example/module"}, @@ -246,7 +261,7 @@ func TestUpdateDeps(t *testing.T) { }, { name: "handles missing src", - srcs: []*GoFile{}, + srcs: []*SourceFile{}, modules: []string{"github.com/example/module"}, rule: &ruleKind{ srcs: []string{"foo.go"}, @@ -282,11 +297,11 @@ func TestUpdateDeps(t *testing.T) { r.AddSrc(src) } - files := make(map[string]*GoFile, len(tc.srcs)) + files := make(map[string]*SourceFile, len(tc.srcs)) srcNames := make([]string, 0, len(tc.srcs)) for _, f := range tc.srcs { - files[f.FileName] = f - srcNames = append(srcNames, f.FileName) + files[f.FileName()] = f + srcNames = append(srcNames, f.FileName()) } err := u.updateRuleDeps(conf, r, []*edit.Rule{}, files) diff --git a/generate/import.go b/generate/import.go index a4bae83..7df4466 100644 --- a/generate/import.go +++ b/generate/import.go @@ -1,6 +1,7 @@ package generate import ( + "context" "go/parser" "go/token" "os" @@ -8,44 +9,127 @@ import ( "strings" "github.com/please-build/puku/kinds" + sitter "github.com/smacker/go-tree-sitter" + "github.com/smacker/go-tree-sitter/typescript/typescript" ) -// GoFile represents a single Go file in a package -type GoFile struct { - // Name is the name from the package clause of this file - Name, FileName string - // Imports are the imports of this file - Imports []string +type fileType string + +const ( + GO fileType = "GO" + TS = "TS" +) + +// SourceFile represents a single source file in the repo. +type SourceFile struct { + name, fileName string + imports []string + dir string + fileType fileType + // // Name is the name from the package clause of this file + // Name() string + // // FileName is the name of the file + // FileName() string + // // Dir is the directory of the file + // Dir() string + // // Imports are the imports of this file + // Imports() []string + + // IsExternal(pkgName string) bool + // IsTest() bool + // IsCmd() bool + + // KindType() kinds.Type +} + +func (f *SourceFile) Name() string { + if f.fileType == TS { + // Remove extension from file name + return strings.TrimSuffix(f.name, filepath.Ext(f.name)) + } + return f.name +} + +func (f *SourceFile) FileName() string { + return f.fileName +} + +func (f *SourceFile) Dir() string { + return f.dir +} + +func (f *SourceFile) Imports() []string { + return f.imports +} + +// IsExternal returns whether the test is external +func (f *SourceFile) IsExternal(pkgName string) bool { + if f.fileType == TS { + return false + } + return f.name == filepath.Base(pkgName)+"_test" && f.IsTest() +} + +func (f *SourceFile) IsTest() bool { + if f.fileType == TS { + return strings.Contains(f.FileName(), ".spec.") + } + return strings.HasSuffix(f.fileName, "_test.go") +} + +func (f *SourceFile) IsCmd() bool { + if f.fileType == TS { + return false + } + return f.name == "main" +} + +func (f *SourceFile) KindType() kinds.Type { + if f.IsTest() { + return kinds.Test + } + if f.IsCmd() { + return kinds.Bin + } + return kinds.Lib } // ImportDir does _some_ of what the go/build ImportDir does but is more permissive. -func ImportDir(dir string) (map[string]*GoFile, error) { +func ImportDir(dir string) (map[string]*SourceFile, error) { files, err := os.ReadDir(dir) if err != nil { return nil, err } - ret := make(map[string]*GoFile, len(files)) + ret := make(map[string]*SourceFile, len(files)) for _, info := range files { if !info.Type().IsRegular() { continue } - if filepath.Ext(info.Name()) != ".go" { - continue + fileExtension := filepath.Ext(info.Name()) + + if fileExtension == ".go" { + f, err := importGoFile(dir, info.Name()) + if err != nil { + return nil, err + } + ret[info.Name()] = f } - f, err := importFile(dir, info.Name()) - if err != nil { - return nil, err + if fileExtension == ".ts" || fileExtension == ".tsx" { + f, err := importTsFile(dir, info.Name()) + if err != nil { + return nil, err + } + ret[info.Name()] = f } - ret[info.Name()] = f } return ret, nil } -func importFile(dir, src string) (*GoFile, error) { +func importGoFile(dir, src string) (*SourceFile, error) { f, err := parser.ParseFile(token.NewFileSet(), filepath.Join(dir, src), nil, parser.ImportsOnly|parser.ParseComments) if err != nil { return nil, err @@ -57,32 +141,97 @@ func importFile(dir, src string) (*GoFile, error) { imports = append(imports, path) } - return &GoFile{ - Name: f.Name.Name, - FileName: src, - Imports: imports, + return &SourceFile{ + name: f.Name.Name, + fileName: src, + imports: imports, + dir: dir, + fileType: GO, }, nil } -// IsExternal returns whether the test is external -func (f *GoFile) IsExternal(pkgName string) bool { - return f.Name == filepath.Base(pkgName)+"_test" && f.IsTest() -} +func importTsFile(dir, src string) (*SourceFile, error) { + tsParser := sitter.NewParser() + tsParser.SetLanguage(typescript.GetLanguage()) -func (f *GoFile) IsTest() bool { - return strings.HasSuffix(f.FileName, "_test.go") -} + sourceCode, err := os.ReadFile(filepath.Join(dir, src)) + if err != nil { + return nil, err + } -func (f *GoFile) IsCmd() bool { - return f.Name == "main" -} + log.Debugf("Parsing TS file: %s\n", filepath.Join(dir, src)) -func (f *GoFile) kindType() kinds.Type { - if f.IsTest() { - return kinds.Test + ctx := context.TODO() + tree, err := tsParser.ParseCtx(ctx, nil, sourceCode) + if err != nil { + return nil, err } - if f.IsCmd() { - return kinds.Bin + + var imports []string + + n := tree.RootNode() + cursor := sitter.NewTreeCursor(n) + defer cursor.Close() + + // enter tree + cursor.GoToFirstChild() + + for true { + node := cursor.CurrentNode() + nodeType := node.Type() + + // we only care about import statements + if nodeType == "import_statement" { + importCursor := sitter.NewTreeCursor(node) + defer importCursor.Close() + importCursor.GoToFirstChild() + + for true { + if importCursor.CurrentFieldName() == "source" { + // remove quotes around string + importPath := string(sourceCode[importCursor.CurrentNode().StartByte()+1 : importCursor.CurrentNode().EndByte()-1]) + imports = append(imports, importPath) + } + + result := importCursor.GoToNextSibling() + if !result { + break + } + } + } + + if nodeType == "export_statement" { + exportCursor := sitter.NewTreeCursor(node) + defer exportCursor.Close() + exportCursor.GoToFirstChild() + + for true { + if exportCursor.CurrentNode().Type() == "from" { + // Go to the next sibling to get from path + exportCursor.GoToNextSibling() + // remove quotes around string + importPath := string(sourceCode[exportCursor.CurrentNode().StartByte()+1 : exportCursor.CurrentNode().EndByte()-1]) + imports = append(imports, importPath) + } + + result := exportCursor.GoToNextSibling() + if !result { + break + } + } + } + + result := cursor.GoToNextSibling() + if !result { + break + } } - return kinds.Lib + + return &SourceFile{ + name: src, + fileName: src, + imports: imports, + dir: dir, + fileType: TS, + }, nil } diff --git a/generate/import_test.go b/generate/import_test.go index 9e1e02e..3290a25 100644 --- a/generate/import_test.go +++ b/generate/import_test.go @@ -19,9 +19,9 @@ func TestImportDir(t *testing.T) { require.NotNil(t, fooTest) require.NotNil(t, externalTest) - assert.Equal(t, foo.Imports, []string{"github.com/example/module"}) - assert.Equal(t, fooTest.Imports, []string{"github.com/stretchr/testify/assert"}) - assert.Equal(t, externalTest.Imports, []string{"github.com/stretchr/testify/require"}) + assert.Equal(t, foo.Imports(), []string{"github.com/example/module"}) + assert.Equal(t, fooTest.Imports(), []string{"github.com/stretchr/testify/assert"}) + assert.Equal(t, externalTest.Imports(), []string{"github.com/stretchr/testify/require"}) assert.False(t, foo.IsTest()) assert.True(t, fooTest.IsTest()) @@ -45,3 +45,32 @@ func TestImportDir(t *testing.T) { require.False(t, main.IsTest()) require.False(t, main.IsExternal("test_project")) } + +func TestImportTSDir(t *testing.T) { + tsDir, err := ImportDir("test_project/ts") + require.NoError(t, err) + + foo := tsDir["foo.ts"] + // fooTest := fooDir["foo_test.go"] + // externalTest := fooDir["external_test.go"] + + require.NotNil(t, foo) + // require.NotNil(t, fooTest) + // require.NotNil(t, externalTest) + + assert.Equal(t, foo.Imports(), []string{"react"}) + // assert.Equal(t, fooTest.Imports, []string{"github.com/stretchr/testify/assert"}) + // assert.Equal(t, externalTest.Imports, []string{"github.com/stretchr/testify/require"}) + + assert.False(t, foo.IsTest()) + // assert.True(t, fooTest.IsTest()) + // assert.True(t, externalTest.IsTest()) + + assert.False(t, foo.IsExternal("foo")) + // assert.False(t, fooTest.IsExternal("foo")) + // assert.True(t, externalTest.IsExternal("foo")) + + assert.False(t, foo.IsCmd()) + // assert.False(t, fooTest.IsCmd()) + // assert.False(t, externalTest.IsCmd()) +} diff --git a/go.mod b/go.mod index 55e3e8a..5ee2ad5 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,10 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/muhammadmuzzammil1998/jsonc v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect + github.com/smacker/go-tree-sitter v0.0.0-20240625050157-a31a98a7c0f6 // indirect github.com/stretchr/objx v0.5.1 // indirect github.com/thought-machine/go-flags v1.6.3 // indirect go.opencensus.io v0.24.0 // indirect diff --git a/go.sum b/go.sum index ec1d21a..0d2d4e7 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/muhammadmuzzammil1998/jsonc v1.0.0 h1:8o5gBQn4ZA3NBA9DlTujCj2a4w0tqWrPVjDwhzkgTIs= +github.com/muhammadmuzzammil1998/jsonc v1.0.0/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU= github.com/peterebden/go-cli-init/v5 v5.2.1 h1:o+7EjS/PiYDvFUQRQVXJRjinmUzDjqcea3nzpjPGj68= github.com/peterebden/go-cli-init/v5 v5.2.1/go.mod h1:0eBDoCJjj3BWyEtidFcP0TlD14cRtOtLCrTG/OVPB74= github.com/please-build/buildtools v0.0.0-20231122153602-22bdf3fe4f1d h1:99mz9ZcfxGKHq/RuNIwh35A3J4LFsKfqQfauJSINwPI= @@ -72,6 +74,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/smacker/go-tree-sitter v0.0.0-20240625050157-a31a98a7c0f6 h1:mtD4ESyObQZnRVxHFcaYp2d7jMBDa4WJRXSB1Vszj+A= +github.com/smacker/go-tree-sitter v0.0.0-20240625050157-a31a98a7c0f6/go.mod h1:q99oHDsbP0xRwmn7Vmob8gbSMNyvJ83OauXPSuHQuKE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -79,6 +83,7 @@ github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= diff --git a/kinds/kinds.go b/kinds/kinds.go index 98079a2..73328dd 100644 --- a/kinds/kinds.go +++ b/kinds/kinds.go @@ -75,4 +75,9 @@ var DefaultKinds = map[string]*Kind{ Name: "go_repo", Type: ThirdParty, }, + "js_library": { + Name: "js_library", + Type: Lib, + SrcsAttr: "srcs", + }, } diff --git a/logging/BUILD b/logging/BUILD index e3f41e1..c89b4fd 100644 --- a/logging/BUILD +++ b/logging/BUILD @@ -4,6 +4,7 @@ go_library( visibility = [ "//:all", "//cmd/puku:all", + "//config:all", "//generate:all", "//graph:all", "//sync:all", diff --git a/plugins/BUILD b/plugins/BUILD index ff41fe0..aabb56f 100644 --- a/plugins/BUILD +++ b/plugins/BUILD @@ -2,5 +2,5 @@ plugin_repo( name = "go", owner = "please-build", plugin = "go-rules", - revision = "v1.21.2", + revision = "v1.17.3", ) diff --git a/test_project/ts/foo.ts b/test_project/ts/foo.ts new file mode 100644 index 0000000..6542df5 --- /dev/null +++ b/test_project/ts/foo.ts @@ -0,0 +1 @@ +import React from "react"; diff --git a/third_party/go/BUILD b/third_party/go/BUILD index 4052e42..acb1d48 100644 --- a/third_party/go/BUILD +++ b/third_party/go/BUILD @@ -319,3 +319,15 @@ go_repo( module = "k8s.io/klog/v2", version = "v2.120.1", ) + +go_repo( + licences = ["MIT"], + module = "github.com/smacker/go-tree-sitter", + version = "v0.0.0-20240625050157-a31a98a7c0f6", +) + +go_repo( + licences = ["MIT"], + module = "github.com/muhammadmuzzammil1998/jsonc", + version = "v1.0.0", +) From e21965250a40fe4ec6ea34bd61d817a96df6b2b6 Mon Sep 17 00:00:00 2001 From: jkim Date: Tue, 27 Aug 2024 15:26:58 +0100 Subject: [PATCH 2/5] Remove go specific check in glob method --- glob/glob.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/glob/glob.go b/glob/glob.go index 4f5932f..956abcc 100644 --- a/glob/glob.go +++ b/glob/glob.go @@ -76,9 +76,9 @@ func (g *Globber) glob(dir, glob string) ([]string, error) { } // We're globbing for Go files to determine their imports. We can skip any other files. - if filepath.Ext(e.Name()) != ".go" { - continue - } + // if filepath.Ext(e.Name()) != ".go" { + // continue + // } match, err := filepath.Match(glob, e.Name()) if err != nil { From f234267dee4d2e98a7808ff9fea4a0f0fa52098a Mon Sep 17 00:00:00 2001 From: jkim Date: Wed, 4 Sep 2024 15:04:08 +0100 Subject: [PATCH 3/5] Convert js_library names to camel case --- generate/generate.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/generate/generate.go b/generate/generate.go index b706b0f..750a19d 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -5,6 +5,7 @@ import ( "io/fs" "os" "path/filepath" + "regexp" "strings" "github.com/please-build/buildtools/build" @@ -489,7 +490,9 @@ func (u *updater) allocateSources(conf *config.Config, pkgDir string, sources ma // Handle TS files if importedFile.fileType == TS { kind = "js_library" - name = importedFile.Name() + // Convention is that js_library target names are camel case. No idea + // why. + name = toSnakeCase(importedFile.Name()) if importedFile.IsTest() { // skip tests for now continue @@ -570,3 +573,12 @@ func (u *updater) unallocatedSources(srcs map[string]*SourceFile, rules []*edit. } return ret, nil } + +var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") +var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + +func toSnakeCase(str string) string { + snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") + snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") + return strings.ToLower(snake) +} From 863f9af2a8e3fe79d52fbead2318a2fbd3b2736f Mon Sep 17 00:00:00 2001 From: jkim Date: Wed, 4 Sep 2024 19:45:11 +0100 Subject: [PATCH 4/5] Extend filename conversion to include "." and special case index files --- generate/generate.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/generate/generate.go b/generate/generate.go index 750a19d..aa5a1bd 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -490,9 +490,14 @@ func (u *updater) allocateSources(conf *config.Config, pkgDir string, sources ma // Handle TS files if importedFile.fileType == TS { kind = "js_library" - // Convention is that js_library target names are camel case. No idea - // why. - name = toSnakeCase(importedFile.Name()) + + // Convention is that js_library target names are camel case apart from + // the index file which is named the same as the folder. + if importedFile.Name() == "index" { + name = filepath.Base(pkgDir) + } else { + name = convertTSFilenameToRuleName(importedFile.Name()) + } if importedFile.IsTest() { // skip tests for now continue @@ -576,9 +581,14 @@ func (u *updater) unallocatedSources(srcs map[string]*SourceFile, rules []*edit. var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") +var matchDots = regexp.MustCompile("\\.") + +func convertTSFilenameToRuleName(str string) string { + // Convert to snake case + ruleName := matchFirstCap.ReplaceAllString(str, "${1}_${2}") + ruleName = matchAllCap.ReplaceAllString(ruleName, "${1}_${2}") -func toSnakeCase(str string) string { - snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") - snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") - return strings.ToLower(snake) + // Replace "." with "_" + ruleName = matchDots.ReplaceAllString(ruleName, "_") + return strings.ToLower(ruleName) } From aeb6a36b9ba2615dc344d211c1c10cc8fc74326d Mon Sep 17 00:00:00 2001 From: jkim Date: Tue, 24 Sep 2024 15:15:13 +0100 Subject: [PATCH 5/5] Add support for async imports --- generate/BUILD | 1 + generate/generate.go | 1 + generate/import.go | 68 ++++++++++++++++++++++++++-------- generate/import_test.go | 22 +++-------- test_project/ts/bar.ts | 1 + test_project/ts/foo.ts | 13 +++++++ test_project/ts/lazy_loaded.ts | 1 + 7 files changed, 75 insertions(+), 32 deletions(-) create mode 100644 test_project/ts/bar.ts create mode 100644 test_project/ts/lazy_loaded.ts diff --git a/generate/BUILD b/generate/BUILD index 69e0616..b4c29a9 100644 --- a/generate/BUILD +++ b/generate/BUILD @@ -17,6 +17,7 @@ go_library( "///third_party/go/github.com_please-build_buildtools//build", "///third_party/go/github.com_please-build_buildtools//labels", "///third_party/go/github.com_smacker_go-tree-sitter//:go-tree-sitter", + "///third_party/go/github.com_smacker_go-tree-sitter//typescript/tsx", "///third_party/go/github.com_smacker_go-tree-sitter//typescript/typescript", "//config", "//edit", diff --git a/generate/generate.go b/generate/generate.go index aa5a1bd..a1ee588 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -507,6 +507,7 @@ func (u *updater) allocateSources(conf *config.Config, pkgDir string, sources ma } else { // Default to assuming we're dealing with a go file kind = "go_library" + name = filepath.Base(pkgDir) if importedFile.IsTest() { name += "_test" kind = "go_test" diff --git a/generate/import.go b/generate/import.go index 7df4466..0cb1c23 100644 --- a/generate/import.go +++ b/generate/import.go @@ -2,6 +2,7 @@ package generate import ( "context" + "fmt" "go/parser" "go/token" "os" @@ -10,6 +11,7 @@ import ( "github.com/please-build/puku/kinds" sitter "github.com/smacker/go-tree-sitter" + "github.com/smacker/go-tree-sitter/typescript/tsx" "github.com/smacker/go-tree-sitter/typescript/typescript" ) @@ -152,7 +154,14 @@ func importGoFile(dir, src string) (*SourceFile, error) { func importTsFile(dir, src string) (*SourceFile, error) { tsParser := sitter.NewParser() - tsParser.SetLanguage(typescript.GetLanguage()) + fileExt := filepath.Ext(src) + if fileExt == ".ts" { + tsParser.SetLanguage(typescript.GetLanguage()) + } else if fileExt == ".tsx" { + tsParser.SetLanguage(tsx.GetLanguage()) + } else { + return nil, fmt.Errorf("unrecognised file extension %q", fileExt) + } sourceCode, err := os.ReadFile(filepath.Join(dir, src)) if err != nil { @@ -167,29 +176,29 @@ func importTsFile(dir, src string) (*SourceFile, error) { return nil, err } - var imports []string + imports := make([]string, 0) n := tree.RootNode() cursor := sitter.NewTreeCursor(n) defer cursor.Close() - // enter tree - cursor.GoToFirstChild() + c := sitter.NewTreeCursor(n) + defer c.Close() - for true { - node := cursor.CurrentNode() - nodeType := node.Type() + var visit func(n *sitter.Node, name string, depth int) + visit = func(n *sitter.Node, name string, depth int) { + nodeType := n.Type() - // we only care about import statements + // handle top level import statements if nodeType == "import_statement" { - importCursor := sitter.NewTreeCursor(node) + importCursor := sitter.NewTreeCursor(n) defer importCursor.Close() importCursor.GoToFirstChild() for true { if importCursor.CurrentFieldName() == "source" { // remove quotes around string - importPath := string(sourceCode[importCursor.CurrentNode().StartByte()+1 : importCursor.CurrentNode().EndByte()-1]) + importPath := extractStringSource(sourceCode, importCursor.CurrentNode()) imports = append(imports, importPath) } @@ -200,8 +209,9 @@ func importTsFile(dir, src string) (*SourceFile, error) { } } + // handle export statements if nodeType == "export_statement" { - exportCursor := sitter.NewTreeCursor(node) + exportCursor := sitter.NewTreeCursor(n) defer exportCursor.Close() exportCursor.GoToFirstChild() @@ -210,7 +220,7 @@ func importTsFile(dir, src string) (*SourceFile, error) { // Go to the next sibling to get from path exportCursor.GoToNextSibling() // remove quotes around string - importPath := string(sourceCode[exportCursor.CurrentNode().StartByte()+1 : exportCursor.CurrentNode().EndByte()-1]) + importPath := extractStringSource(sourceCode, exportCursor.CurrentNode()) imports = append(imports, importPath) } @@ -221,11 +231,34 @@ func importTsFile(dir, src string) (*SourceFile, error) { } } - result := cursor.GoToNextSibling() - if !result { - break + // handle async imports + if nodeType == "call_expression" { + callCursor := sitter.NewTreeCursor(n) + defer callCursor.Close() + + callNode := callCursor.CurrentNode() + for i := 0; i < int(callCursor.CurrentNode().ChildCount()); i++ { + child := callNode.Child(i) + + if child.Type() == "import" && callNode.FieldNameForChild(i) == "function" { + // arguments should be the next child + argumentNode := callNode.Child(i + 1) + for i := 0; i < int(argumentNode.ChildCount()); i++ { + if argumentNode.Child(i).Type() == "string" { + importPath := extractStringSource(sourceCode, argumentNode.Child(i)) + imports = append(imports, importPath) + } + } + } + } + } + + for i := 0; i < int(n.ChildCount()); i++ { + visit(n.Child(i), n.FieldNameForChild(i), depth+1) } + } + visit(cursor.CurrentNode(), "root", 0) return &SourceFile{ name: src, @@ -235,3 +268,8 @@ func importTsFile(dir, src string) (*SourceFile, error) { fileType: TS, }, nil } + +func extractStringSource(sourceCode []byte, n *sitter.Node) string { + stringSource := string(sourceCode[n.StartByte()+1 : n.EndByte()-1]) + return stringSource +} diff --git a/generate/import_test.go b/generate/import_test.go index 3290a25..52f5aa7 100644 --- a/generate/import_test.go +++ b/generate/import_test.go @@ -51,26 +51,14 @@ func TestImportTSDir(t *testing.T) { require.NoError(t, err) foo := tsDir["foo.ts"] - // fooTest := fooDir["foo_test.go"] - // externalTest := fooDir["external_test.go"] + bar := tsDir["bar.ts"] require.NotNil(t, foo) - // require.NotNil(t, fooTest) - // require.NotNil(t, externalTest) + require.NotNil(t, bar) - assert.Equal(t, foo.Imports(), []string{"react"}) - // assert.Equal(t, fooTest.Imports, []string{"github.com/stretchr/testify/assert"}) - // assert.Equal(t, externalTest.Imports, []string{"github.com/stretchr/testify/require"}) + assert.Equal(t, foo.Imports(), []string{"react", "./bar", "./lazy_loaded"}) + assert.Equal(t, bar.Imports(), []string{}) assert.False(t, foo.IsTest()) - // assert.True(t, fooTest.IsTest()) - // assert.True(t, externalTest.IsTest()) - - assert.False(t, foo.IsExternal("foo")) - // assert.False(t, fooTest.IsExternal("foo")) - // assert.True(t, externalTest.IsExternal("foo")) - - assert.False(t, foo.IsCmd()) - // assert.False(t, fooTest.IsCmd()) - // assert.False(t, externalTest.IsCmd()) + assert.False(t, bar.IsTest()) } diff --git a/test_project/ts/bar.ts b/test_project/ts/bar.ts new file mode 100644 index 0000000..e64329c --- /dev/null +++ b/test_project/ts/bar.ts @@ -0,0 +1 @@ +export function bar() {} diff --git a/test_project/ts/foo.ts b/test_project/ts/foo.ts index 6542df5..7046c71 100644 --- a/test_project/ts/foo.ts +++ b/test_project/ts/foo.ts @@ -1 +1,14 @@ import React from "react"; + +import { bar } from "./bar"; + +// Test an async import +export const routes = { + "/some/path": { + loader() { + const { lazyLoadedFn } = await import("./lazy_loaded"); + // dynamic import paths should not work + await import(`./lazy_${1 + 1}_loaded`); + }, + }, +}; diff --git a/test_project/ts/lazy_loaded.ts b/test_project/ts/lazy_loaded.ts new file mode 100644 index 0000000..40bca84 --- /dev/null +++ b/test_project/ts/lazy_loaded.ts @@ -0,0 +1 @@ +export const laztLoadedFn = () => {};