diff --git a/README.md b/README.md index 70e1ea5..98ec0c9 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ This is what generates expressions from tokens. - [x] Unused variables - [x] Unused parameters -- [ ] Unused imports +- [x] Unused imports - [ ] Unnecessary newlines - [ ] Ineffective assignments diff --git a/src/build/Build.go b/src/build/Build.go index b292f4b..7f407ce 100644 --- a/src/build/Build.go +++ b/src/build/Build.go @@ -20,10 +20,10 @@ func New(files ...string) *Build { } } -// Run parses the input files and generates an executable file. +// Run compiles the input files. func (build *Build) Run() (compiler.Result, error) { - functions, errors := scanner.Scan(build.Files) - return compiler.Compile(functions, errors) + files, functions, errors := scanner.Scan(build.Files) + return compiler.Compile(files, functions, errors) } // Executable returns the path to the executable. diff --git a/src/build/compiler/Compile.go b/src/build/compiler/Compile.go index e25de67..a10224d 100644 --- a/src/build/compiler/Compile.go +++ b/src/build/compiler/Compile.go @@ -5,15 +5,34 @@ import ( "git.akyoto.dev/cli/q/src/build/core" "git.akyoto.dev/cli/q/src/build/errors" + "git.akyoto.dev/cli/q/src/build/fs" ) // Compile waits for the scan to finish and compiles all functions. -func Compile(functions <-chan *core.Function, errs <-chan error) (Result, error) { +func Compile(files <-chan *fs.File, functions <-chan *core.Function, errs <-chan error) (Result, error) { result := Result{} - all := map[string]*core.Function{} + allFunctions := map[string]*core.Function{} + allFiles := map[string]*fs.File{} - for functions != nil || errs != nil { + for functions != nil || files != nil || errs != nil { select { + case function, ok := <-functions: + if !ok { + functions = nil + continue + } + + function.Functions = allFunctions + allFunctions[function.Name] = function + + case file, ok := <-files: + if !ok { + files = nil + continue + } + + allFiles[file.Path] = file + case err, ok := <-errs: if !ok { errs = nil @@ -21,23 +40,14 @@ func Compile(functions <-chan *core.Function, errs <-chan error) (Result, error) } return result, err - - case function, ok := <-functions: - if !ok { - functions = nil - continue - } - - function.Functions = all - all[function.Name] = function } } // Start parallel compilation - CompileFunctions(all) + CompileFunctions(allFunctions) // Report errors if any occurred - for _, function := range all { + for _, function := range allFunctions { if function.Err != nil { return result, function.Err } @@ -46,15 +56,24 @@ func Compile(functions <-chan *core.Function, errs <-chan error) (Result, error) result.DataCount += len(function.Assembler.Data) } + // Check for unused imports in all files + for _, file := range allFiles { + for _, pkg := range file.Imports { + if !pkg.Used { + return result, errors.New(&errors.UnusedImport{Package: pkg.Path}, file, pkg.Position) + } + } + } + // Check for existence of `main` - main, exists := all["main.main"] + main, exists := allFunctions["main.main"] if !exists { return result, errors.MissingMainFunction } result.Main = main - result.Functions = all + result.Functions = allFunctions return result, nil } diff --git a/src/build/core/CompileCall.go b/src/build/core/CompileCall.go index 7ac3d10..68e915a 100644 --- a/src/build/core/CompileCall.go +++ b/src/build/core/CompileCall.go @@ -30,6 +30,20 @@ func (f *Function) CompileCall(root *expression.Expression) error { isSyscall := name == "syscall" if !isSyscall { + if pkg != f.File.Package { + if f.File.Imports == nil { + return errors.New(&errors.UnknownPackage{Name: pkg}, f.File, nameRoot.Token.Position) + } + + imp, exists := f.File.Imports[pkg] + + if !exists { + return errors.New(&errors.UnknownPackage{Name: pkg}, f.File, nameRoot.Token.Position) + } + + imp.Used = true + } + tmp := strings.Builder{} tmp.WriteString(pkg) tmp.WriteString(".") @@ -38,7 +52,7 @@ func (f *Function) CompileCall(root *expression.Expression) error { _, exists := f.Functions[fullName] if !exists { - return errors.New(&errors.UnknownFunction{Name: name}, f.File, root.Children[0].Token.Position) + return errors.New(&errors.UnknownFunction{Name: name}, f.File, nameRoot.Token.Position) } } diff --git a/src/build/errors/UnknownPackage.go b/src/build/errors/UnknownPackage.go new file mode 100644 index 0000000..5cdc965 --- /dev/null +++ b/src/build/errors/UnknownPackage.go @@ -0,0 +1,18 @@ +package errors + +import "fmt" + +// UnknownPackage represents unknown package errors. +type UnknownPackage struct { + Name string + CorrectName string +} + +// Error generates the string representation. +func (err *UnknownPackage) Error() string { + if err.CorrectName != "" { + return fmt.Sprintf("Unknown package '%s', did you mean '%s'?", err.Name, err.CorrectName) + } + + return fmt.Sprintf("Unknown package '%s'", err.Name) +} diff --git a/src/build/errors/UnusedImport.go b/src/build/errors/UnusedImport.go new file mode 100644 index 0000000..441e9d9 --- /dev/null +++ b/src/build/errors/UnusedImport.go @@ -0,0 +1,13 @@ +package errors + +import "fmt" + +// UnusedImport error is created when an import is never used. +type UnusedImport struct { + Package string +} + +// Error generates the string representation. +func (err *UnusedImport) Error() string { + return fmt.Sprintf("Unused import '%s'", err.Package) +} diff --git a/src/build/fs/File.go b/src/build/fs/File.go index 1a10a1d..205daa1 100644 --- a/src/build/fs/File.go +++ b/src/build/fs/File.go @@ -4,7 +4,9 @@ import "git.akyoto.dev/cli/q/src/build/token" // File represents a single source file. type File struct { - Path string - Bytes []byte - Tokens token.List + Path string + Package string + Bytes []byte + Imports map[string]*Import + Tokens token.List } diff --git a/src/build/fs/Import.go b/src/build/fs/Import.go new file mode 100644 index 0000000..6690425 --- /dev/null +++ b/src/build/fs/Import.go @@ -0,0 +1,11 @@ +package fs + +import "git.akyoto.dev/cli/q/src/build/token" + +// Import represents an import statement in a file. +type Import struct { + Path string + FullPath string + Position token.Position + Used bool +} diff --git a/src/build/scanner/Scan.go b/src/build/scanner/Scan.go index b79d04f..4b25a3d 100644 --- a/src/build/scanner/Scan.go +++ b/src/build/scanner/Scan.go @@ -1,10 +1,14 @@ package scanner -import "git.akyoto.dev/cli/q/src/build/core" +import ( + "git.akyoto.dev/cli/q/src/build/core" + "git.akyoto.dev/cli/q/src/build/fs" +) // Scan scans the list of files. -func Scan(files []string) (<-chan *core.Function, <-chan error) { +func Scan(files []string) (<-chan *fs.File, <-chan *core.Function, <-chan error) { scanner := Scanner{ + files: make(chan *fs.File), functions: make(chan *core.Function), errors: make(chan error), } @@ -12,9 +16,10 @@ func Scan(files []string) (<-chan *core.Function, <-chan error) { go func() { scanner.queue(files...) scanner.group.Wait() + close(scanner.files) close(scanner.functions) close(scanner.errors) }() - return scanner.functions, scanner.errors + return scanner.files, scanner.functions, scanner.errors } diff --git a/src/build/scanner/Scanner.go b/src/build/scanner/Scanner.go index 0c829ac..5e6b0c2 100644 --- a/src/build/scanner/Scanner.go +++ b/src/build/scanner/Scanner.go @@ -4,10 +4,12 @@ import ( "sync" "git.akyoto.dev/cli/q/src/build/core" + "git.akyoto.dev/cli/q/src/build/fs" ) // Scanner is used to scan files before the actual compilation step. type Scanner struct { + files chan *fs.File functions chan *core.Function errors chan error queued sync.Map diff --git a/src/build/scanner/scanFile.go b/src/build/scanner/scanFile.go index 4d7b620..757cbaf 100644 --- a/src/build/scanner/scanFile.go +++ b/src/build/scanner/scanFile.go @@ -26,11 +26,14 @@ func (s *Scanner) scanFile(path string, pkg string) error { tokens := token.Tokenize(contents) file := &fs.File{ - Path: path, - Bytes: contents, - Tokens: tokens, + Path: path, + Bytes: contents, + Tokens: tokens, + Package: pkg, } + s.files <- file + var ( i = 0 groupLevel = 0 @@ -50,8 +53,20 @@ func (s *Scanner) scanFile(path string, pkg string) error { } packageName := tokens[i].Text(contents) - s.queueDirectory(filepath.Join(config.Library, packageName), packageName) + if file.Imports == nil { + file.Imports = map[string]*fs.Import{} + } + + fullPath := filepath.Join(config.Library, packageName) + + file.Imports[packageName] = &fs.Import{ + Path: packageName, + FullPath: fullPath, + Position: tokens[i].Position, + } + + s.queueDirectory(fullPath, packageName) i++ if tokens[i].Kind != token.NewLine && tokens[i].Kind != token.EOF { diff --git a/tests/errors/UnknownPackage.q b/tests/errors/UnknownPackage.q new file mode 100644 index 0000000..a159f13 --- /dev/null +++ b/tests/errors/UnknownPackage.q @@ -0,0 +1,3 @@ +main() { + sys.read() +} \ No newline at end of file diff --git a/tests/errors/UnusedImport.q b/tests/errors/UnusedImport.q new file mode 100644 index 0000000..e5a5498 --- /dev/null +++ b/tests/errors/UnusedImport.q @@ -0,0 +1,3 @@ +import sys + +main(){} \ No newline at end of file diff --git a/tests/errors_test.go b/tests/errors_test.go index 88f6b3f..2b9ce40 100644 --- a/tests/errors_test.go +++ b/tests/errors_test.go @@ -40,6 +40,8 @@ var errs = []struct { {"UnknownIdentifier.q", &errors.UnknownIdentifier{Name: "x"}}, {"UnknownIdentifier2.q", &errors.UnknownIdentifier{Name: "x"}}, {"UnknownIdentifier3.q", &errors.UnknownIdentifier{Name: "x"}}, + {"UnknownPackage.q", &errors.UnknownPackage{Name: "sys"}}, + {"UnusedImport.q", &errors.UnusedImport{Package: "sys"}}, {"UnusedVariable.q", &errors.UnusedVariable{Name: "x"}}, }