Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dart Language Parser #91

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"path/filepath"

"github.com/gabotechs/dep-tree/internal/config"
"github.com/gabotechs/dep-tree/internal/dart"
"github.com/gabotechs/dep-tree/internal/dummy"
"github.com/gabotechs/dep-tree/internal/js"
"github.com/gabotechs/dep-tree/internal/language"
Expand Down Expand Up @@ -99,6 +100,7 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
python int
rust int
dummy int
dart int
}{}
top := struct {
lang string
Expand Down Expand Up @@ -130,6 +132,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 == "" {
Expand All @@ -142,6 +150,8 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
return rust.MakeRustLanguage(&cfg.Rust)
case "python":
return python.MakePythonLanguage(&cfg.Python)
case "dart":
return &dart.Language{}, nil
case "dummy":
return &dummy.Language{}, nil
default:
Expand Down
83 changes: 83 additions & 0 deletions internal/dart/language.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 <root-where-pubspec-is-located>/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)
}

// fmt.Println(importPath)
result.Imports = append(result.Imports, language.ImportEntry{
AbsPath: 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 <root-where-pubspec-is-located>/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)
}

// fmt.Println(exportPath)
result.Exports = append(result.Exports, language.ExportEntry{
AbsPath: exportPath,
})
}
}

return &result, nil
}
83 changes: 83 additions & 0 deletions internal/dart/parser.go
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 39 additions & 0 deletions internal/dart/resolve.go
Original file line number Diff line number Diff line change
@@ -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
}
}