package modules
import (
"fmt"
"os"
"path/filepath"
"github.com/marzeq/quokka/parser"
"github.com/marzeq/quokka/tokeniser"
)
type FileNode struct {
Path string
Imports []*FileNode
}
type DepGraph struct {
Files map[string]*FileNode
ASTs map[string]*parser.RootNode
}
func NewDepGraph(path string, rn *parser.RootNode) (*DepGraph, error) {
dg := &DepGraph{
Files: make(map[string]*FileNode),
ASTs: make(map[string]*parser.RootNode),
}
err := dg.Construct(path, rn)
if err != nil {
return nil, err
}
return dg, nil
}
var moduleSearchPaths = []string{
".",
"/usr/share/quokka/lib",
"/usr/local/share/quokka/lib",
filepath.Join(os.Getenv("HOME"), ".local/share/quokka/lib"),
}
func (g *DepGraph) Construct(path string, rn *parser.RootNode) error {
path, err := filepath.Abs(path)
if err != nil {
return err
}
if _, ok := g.ASTs[path]; ok {
return nil
}
node := &FileNode{
Path: path,
Imports: []*FileNode{},
}
g.Files[path] = node
g.ASTs[path] = rn
allFiles, err := findSrcFiles()
if err != nil {
return err
}
for _, moduleName := range getImports(rn) {
moduleFilesFound := false
for _, f := range allFiles {
isCwd := false
absF, _ := filepath.Abs(f)
cwd, _ := filepath.Abs(".")
if filepath.Dir(absF) == cwd {
isCwd = true
}
t, err := tokeniser.NewTokeniserFromFile(f)
if err != nil {
if isCwd {
return fmt.Errorf("failed to tokenise %s: %w", f, err)
}
continue
}
toks, err := t.Tokenise()
if err != nil {
if isCwd {
return fmt.Errorf("failed to tokenise %s: %w", f, err)
}
continue
}
p := parser.NewParser(toks)
ast, err := p.Parse()
if err != nil || len(ast.Body) == 0 {
if isCwd {
return fmt.Errorf("failed to parse %s: %w", f, err)
}
continue
}
if mn, ok := ast.Body[0].(*parser.ModuleNode); !ok || mn.Name != moduleName {
continue
}
moduleFilesFound = true
if err := g.Construct(f, ast); err != nil {
return err
}
node.Imports = append(node.Imports, g.Files[f])
}
if !moduleFilesFound {
return fmt.Errorf("module not found: %s", moduleName)
}
}
return nil
}
func getImports(rn *parser.RootNode) []string {
modules := []string{}
for _, n := range rn.Body {
if imp, ok := n.(*parser.ImportNode); ok {
modules = append(modules, imp.Modules...)
}
}
return modules
}
func findSrcFiles() ([]string, error) {
var result []string
seen := map[string]struct{}{}
for _, dir := range moduleSearchPaths {
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
if filepath.Ext(path) != ".qk" {
return nil
}
if _, ok := seen[path]; ok {
return nil
}
seen[path] = struct{}{}
result = append(result, path)
return nil
})
if err != nil {
return nil, err
}
}
return result, nil
}
func (g *DepGraph) TopoSort() ([]string, error) {
visited := map[string]bool{}
temp := map[string]bool{}
result := []string{}
var visit func(n *FileNode) error
visit = func(n *FileNode) error {
if n == nil {
return nil
}
if temp[n.Path] {
return fmt.Errorf("cycle detected at file: %s", n.Path)
}
if visited[n.Path] {
return nil
}
temp[n.Path] = true
for _, child := range n.Imports {
if err := visit(child); err != nil {
return err
}
}
temp[n.Path] = false
visited[n.Path] = true
result = append(result, n.Path)
return nil
}
for _, n := range g.Files {
if !visited[n.Path] {
if err := visit(n); err != nil {
return nil, err
}
}
}
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return result, nil
}