diff --git a/cmd/root.go b/cmd/root.go index 200da8b..b0cb248 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/gabotechs/dep-tree/internal/config" + "github.com/gabotechs/dep-tree/internal/dart" "github.com/gabotechs/dep-tree/internal/dummy" golang "github.com/gabotechs/dep-tree/internal/go" "github.com/gabotechs/dep-tree/internal/graph" @@ -142,6 +143,7 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) { rust int golang int dummy int + dart int }{} top := struct { lang string @@ -179,6 +181,12 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) { top.v = score.dummy top.lang = "dummy" } + case utils.EndsWith(file, dart.Extensions): + score.dart += 1 + if score.dart > top.v { + top.v = score.dart + top.lang = "dart" + } } } if top.lang == "" { @@ -193,6 +201,8 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) { return python.MakePythonLanguage(&cfg.Python) case "golang": return golang.NewLanguage(files[0], &cfg.Golang) + case "dart": + return &dart.Language{}, nil case "dummy": return &dummy.Language{}, nil default: diff --git a/internal/dart/language.go b/internal/dart/language.go new file mode 100644 index 0000000..ed6b8b4 --- /dev/null +++ b/internal/dart/language.go @@ -0,0 +1,79 @@ +package dart + +import ( + "bytes" + "os" + "path/filepath" + + "github.com/gabotechs/dep-tree/internal/language" +) + +var Extensions = []string{"dart"} + +type Language struct{} + +func (l *Language) ParseFile(path string) (*language.FileInfo, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + file, err := ParseFile(path) + if err != nil { + return nil, err + } + currentDir, _ := os.Getwd() + relPath, _ := filepath.Rel(currentDir, path) + return &language.FileInfo{ + Content: file.Statements, // dump the parsed statements into the FileInfo struct. + Loc: bytes.Count(content, []byte("\n")), // get the amount of lines of code. + Size: len(content), // get the size of the file in bytes. + AbsPath: path, // provide its absolute path. + RelPath: relPath, // provide the path relative to the current dir. + }, nil +} + +func (l *Language) ParseImports(file *language.FileInfo) (*language.ImportsResult, error) { + var result language.ImportsResult + + for _, statement := range file.Content.([]Statement) { + if statement.Import != nil { + var importPath string + + if statement.Import.IsAbsolute { + // Code files must always be in the /lib directory. + importPath = filepath.Join(findClosestDartRootDir(file.AbsPath), "lib", statement.Import.From) + } else { + // Relative imports are relative to the current file. + importPath = filepath.Join(filepath.Dir(file.AbsPath), statement.Import.From) + } + + result.Imports = append(result.Imports, language.EmptyImport(importPath)) + } + } + + return &result, nil +} + +func (l *Language) ParseExports(file *language.FileInfo) (*language.ExportsResult, error) { + var result language.ExportsResult + + for _, statement := range file.Content.([]Statement) { + if statement.Export != nil { + var exportPath string + + if statement.Export.IsAbsolute { + // Code files must always be in the /lib directory. + exportPath = filepath.Join(findClosestDartRootDir(file.AbsPath), "lib", statement.Export.From) + } else { + // Relative imports are relative to the current file. + exportPath = filepath.Join(filepath.Dir(file.AbsPath), statement.Export.From) + } + + result.Exports = append(result.Exports, language.ExportEntry{ + AbsPath: exportPath, + }) + } + } + + return &result, nil +} diff --git a/internal/dart/parser.go b/internal/dart/parser.go new file mode 100644 index 0000000..56cbf37 --- /dev/null +++ b/internal/dart/parser.go @@ -0,0 +1,83 @@ +package dart + +import ( + "bufio" + "os" + "regexp" + "strings" +) + +var packageRegex = regexp.MustCompile(`package:[^/]+\/`) +var importRegex = regexp.MustCompile(`import\s+(['"])(.*?\.dart)`) +var exportRegex = regexp.MustCompile(`export\s+(['"])(.*?\.dart)`) + +type ImportStatement struct { + From string + IsAbsolute bool +} + +type ExportStatement struct { + From string + IsAbsolute bool +} + +type Statement struct { + Import *ImportStatement + Export *ExportStatement +} + +type File struct { + Statements []Statement +} + +func ParseFile(path string) (*File, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + var fileData File + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + + // Remove comments + if idx := strings.Index(line, "//"); idx != -1 { + line = line[:idx] + } + + line = strings.TrimSpace(line) + + // Remove package patterns from the line and determine if the import is absolute + originalLine := line // Keep the original line to check for package later + line = packageRegex.ReplaceAllString(line, "") + + // Check if the package pattern was matched to set IsAbsolute + isAbsolute := line != originalLine + + if importMatch := importRegex.FindStringSubmatch(line); importMatch != nil { + fileData.Statements = append(fileData.Statements, Statement{ + Import: &ImportStatement{ + From: importMatch[2], + IsAbsolute: isAbsolute, + }, + }) + } else if exportMatch := exportRegex.FindStringSubmatch(line); exportMatch != nil { + fileData.Statements = append(fileData.Statements, Statement{ + Import: &ImportStatement{ // Treat exports like imports! + From: exportMatch[2], + IsAbsolute: isAbsolute, + }, + Export: &ExportStatement{ + From: exportMatch[2], + IsAbsolute: isAbsolute, + }, + }) + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + return &fileData, nil +} diff --git a/internal/dart/resolve.go b/internal/dart/resolve.go new file mode 100644 index 0000000..b62ae60 --- /dev/null +++ b/internal/dart/resolve.go @@ -0,0 +1,39 @@ +package dart + +import ( + "os" + "path/filepath" + "sync" +) + +// rootDir stores the found root directory to avoid repeated filesystem checks. +var rootDir string +var lock sync.Once + +// findClosestDartRootDir finds the closest directory from the given path that contains a Dart project root indicator file. +// It caches the result after the first filesystem scan and reuses it for subsequent calls. +func findClosestDartRootDir(path string) string { + lock.Do(func() { + setRootDir(path) + }) + return rootDir +} + +// setRootDir performs the filesystem traversal to locate the root directory. +func setRootDir(path string) { + var rootIndicatorFiles = []string{"pubspec.yaml", "pubspec.yml"} + currentPath := path + for { + for _, file := range rootIndicatorFiles { + if _, err := os.Stat(filepath.Join(currentPath, file)); err == nil { + rootDir = currentPath + return + } + } + parentDir := filepath.Dir(currentPath) + if parentDir == currentPath { + panic("no Dart project root found. Make sure there is a pubspec.yaml or pubspec.yml in the project root.") + } + currentPath = parentDir + } +}