Added a check for unused imports

This commit is contained in:
Eduard Urbach 2024-07-31 11:55:21 +02:00
parent 2293603923
commit 87ae78c86a
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
14 changed files with 138 additions and 31 deletions

View File

@ -143,7 +143,7 @@ This is what generates expressions from tokens.
- [x] Unused variables - [x] Unused variables
- [x] Unused parameters - [x] Unused parameters
- [ ] Unused imports - [x] Unused imports
- [ ] Unnecessary newlines - [ ] Unnecessary newlines
- [ ] Ineffective assignments - [ ] Ineffective assignments

View File

@ -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) { func (build *Build) Run() (compiler.Result, error) {
functions, errors := scanner.Scan(build.Files) files, functions, errors := scanner.Scan(build.Files)
return compiler.Compile(functions, errors) return compiler.Compile(files, functions, errors)
} }
// Executable returns the path to the executable. // Executable returns the path to the executable.

View File

@ -5,15 +5,34 @@ import (
"git.akyoto.dev/cli/q/src/build/core" "git.akyoto.dev/cli/q/src/build/core"
"git.akyoto.dev/cli/q/src/build/errors" "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. // 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{} 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 { 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: case err, ok := <-errs:
if !ok { if !ok {
errs = nil errs = nil
@ -21,23 +40,14 @@ func Compile(functions <-chan *core.Function, errs <-chan error) (Result, error)
} }
return result, err return result, err
case function, ok := <-functions:
if !ok {
functions = nil
continue
}
function.Functions = all
all[function.Name] = function
} }
} }
// Start parallel compilation // Start parallel compilation
CompileFunctions(all) CompileFunctions(allFunctions)
// Report errors if any occurred // Report errors if any occurred
for _, function := range all { for _, function := range allFunctions {
if function.Err != nil { if function.Err != nil {
return result, function.Err 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) 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` // Check for existence of `main`
main, exists := all["main.main"] main, exists := allFunctions["main.main"]
if !exists { if !exists {
return result, errors.MissingMainFunction return result, errors.MissingMainFunction
} }
result.Main = main result.Main = main
result.Functions = all result.Functions = allFunctions
return result, nil return result, nil
} }

View File

@ -30,6 +30,20 @@ func (f *Function) CompileCall(root *expression.Expression) error {
isSyscall := name == "syscall" isSyscall := name == "syscall"
if !isSyscall { 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 := strings.Builder{}
tmp.WriteString(pkg) tmp.WriteString(pkg)
tmp.WriteString(".") tmp.WriteString(".")
@ -38,7 +52,7 @@ func (f *Function) CompileCall(root *expression.Expression) error {
_, exists := f.Functions[fullName] _, exists := f.Functions[fullName]
if !exists { 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)
} }
} }

View File

@ -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)
}

View File

@ -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)
}

View File

@ -5,6 +5,8 @@ import "git.akyoto.dev/cli/q/src/build/token"
// File represents a single source file. // File represents a single source file.
type File struct { type File struct {
Path string Path string
Package string
Bytes []byte Bytes []byte
Imports map[string]*Import
Tokens token.List Tokens token.List
} }

11
src/build/fs/Import.go Normal file
View File

@ -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
}

View File

@ -1,10 +1,14 @@
package scanner 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. // 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{ scanner := Scanner{
files: make(chan *fs.File),
functions: make(chan *core.Function), functions: make(chan *core.Function),
errors: make(chan error), errors: make(chan error),
} }
@ -12,9 +16,10 @@ func Scan(files []string) (<-chan *core.Function, <-chan error) {
go func() { go func() {
scanner.queue(files...) scanner.queue(files...)
scanner.group.Wait() scanner.group.Wait()
close(scanner.files)
close(scanner.functions) close(scanner.functions)
close(scanner.errors) close(scanner.errors)
}() }()
return scanner.functions, scanner.errors return scanner.files, scanner.functions, scanner.errors
} }

View File

@ -4,10 +4,12 @@ import (
"sync" "sync"
"git.akyoto.dev/cli/q/src/build/core" "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. // Scanner is used to scan files before the actual compilation step.
type Scanner struct { type Scanner struct {
files chan *fs.File
functions chan *core.Function functions chan *core.Function
errors chan error errors chan error
queued sync.Map queued sync.Map

View File

@ -29,8 +29,11 @@ func (s *Scanner) scanFile(path string, pkg string) error {
Path: path, Path: path,
Bytes: contents, Bytes: contents,
Tokens: tokens, Tokens: tokens,
Package: pkg,
} }
s.files <- file
var ( var (
i = 0 i = 0
groupLevel = 0 groupLevel = 0
@ -50,8 +53,20 @@ func (s *Scanner) scanFile(path string, pkg string) error {
} }
packageName := tokens[i].Text(contents) 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++ i++
if tokens[i].Kind != token.NewLine && tokens[i].Kind != token.EOF { if tokens[i].Kind != token.NewLine && tokens[i].Kind != token.EOF {

View File

@ -0,0 +1,3 @@
main() {
sys.read()
}

View File

@ -0,0 +1,3 @@
import sys
main(){}

View File

@ -40,6 +40,8 @@ var errs = []struct {
{"UnknownIdentifier.q", &errors.UnknownIdentifier{Name: "x"}}, {"UnknownIdentifier.q", &errors.UnknownIdentifier{Name: "x"}},
{"UnknownIdentifier2.q", &errors.UnknownIdentifier{Name: "x"}}, {"UnknownIdentifier2.q", &errors.UnknownIdentifier{Name: "x"}},
{"UnknownIdentifier3.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"}}, {"UnusedVariable.q", &errors.UnusedVariable{Name: "x"}},
} }