diff --git a/.gitignore b/.gitignore index 316825f2..78bf9b35 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ go.work # Go binaries solgo -examples/examples +prototype/* diff --git a/contextual_solidity_parser.go b/contextual_solidity_parser.go deleted file mode 100644 index 8cc4db60..00000000 --- a/contextual_solidity_parser.go +++ /dev/null @@ -1,39 +0,0 @@ -package solgo - -import "github.com/txpull/solgo/parser" - -// ContextualSolidityParser is a wrapper around the SolidityParser that maintains a stack of contexts. -// This allows the parser to keep track of the current context (e.g., within a contract definition, function definition, etc.) -// as it parses a Solidity contract. -type ContextualSolidityParser struct { - *parser.SolidityParser // SolidityParser is the base parser from the Solidity parser. - contextStack []string // contextStack is a stack of contexts. Each context corresponds to a rule in the grammar. -} - -// PushContext pushes a new context onto the context stack. This should be called when the parser enters a new rule. -func (p *ContextualSolidityParser) PushContext(context string) { - p.contextStack = append(p.contextStack, context) -} - -// PopContext pops the current context from the context stack. This should be called when the parser exits a rule. -func (p *ContextualSolidityParser) PopContext() { - p.contextStack = p.contextStack[:len(p.contextStack)-1] -} - -// CurrentContext returns the current context, i.e., the context at the top of the stack. -// If the stack is empty, it returns an empty string. -func (p *ContextualSolidityParser) CurrentContext() string { - if len(p.contextStack) == 0 { - return "" - } - return p.contextStack[len(p.contextStack)-1] -} - -// ContractDefinition is called when the parser enters a contract definition. -// It pushes "ContractDefinition" onto the context stack, calls the original ContractDefinition method, -// and then pops the context from the stack before returning. -func (p *ContextualSolidityParser) ContractDefinition() parser.IContractDefinitionContext { - p.PushContext("ContractDefinition") - defer p.PopContext() - return p.SolidityParser.ContractDefinition() // Call the original method -} diff --git a/contextual_solidity_parser_test.go b/contextual_solidity_parser_test.go deleted file mode 100644 index a3e3c3fc..00000000 --- a/contextual_solidity_parser_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package solgo - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPushContext(t *testing.T) { - parser := &ContextualSolidityParser{ - contextStack: []string{}, - } - - parser.PushContext("ContractDefinition") - - assert.Equal(t, 1, len(parser.contextStack), "Expected context stack length to be 1") - assert.Equal(t, "ContractDefinition", parser.contextStack[0], "Expected context to be 'ContractDefinition'") -} - -func TestPopContext(t *testing.T) { - parser := &ContextualSolidityParser{ - contextStack: []string{"ContractDefinition"}, - } - - parser.PopContext() - - assert.Equal(t, 0, len(parser.contextStack), "Expected context stack length to be 0") -} - -func TestCurrentContext(t *testing.T) { - parser := &ContextualSolidityParser{ - contextStack: []string{"ContractDefinition", "FunctionDeclaration"}, - } - - context := parser.CurrentContext() - - assert.Equal(t, "FunctionDeclaration", context, "Expected current context to be 'FunctionDeclaration'") -} - -func TestCurrentContextEmpty(t *testing.T) { - parser := &ContextualSolidityParser{ - contextStack: []string{}, - } - - context := parser.CurrentContext() - - assert.Equal(t, "", context, "Expected current context to be an empty string") -} diff --git a/data/tests/BuggyContract.sol b/data/tests/BuggyContract.sol new file mode 100644 index 00000000..0f49b7f3 --- /dev/null +++ b/data/tests/BuggyContract.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.8.0; + +contract TestContract { + uint256 public count; + + // Missing semicolon + function increment() public { + count += 1 + } + + // Mismatched parentheses + function decrement() public { + count -= 1; + } + + // Missing function keyword + setCount(uint256 _count) public { + count = _count; + } + + // Extraneous input 'returns' + function getCount() public returns (uint256) { + return count + } +} \ No newline at end of file diff --git a/listeners.go b/listeners.go index 43d32b28..3b471012 100644 --- a/listeners.go +++ b/listeners.go @@ -14,8 +14,13 @@ const ( ListenerAbi ListenerName = "abi" ListenerContractInfo ListenerName = "contract_info" ListenerAst ListenerName = "ast" + ListenerSyntaxErrors ListenerName = "syntax_errors" ) +func (l ListenerName) String() string { + return string(l) +} + type listeners map[ListenerName]antlr.ParseTreeListener func (s *SolGo) RegisterListener(name ListenerName, listener antlr.ParseTreeListener) error { diff --git a/solgo.go b/solgo.go index 1ef545d1..4cd19e53 100644 --- a/solgo.go +++ b/solgo.go @@ -7,6 +7,8 @@ import ( "github.com/antlr4-go/antlr/v4" "github.com/txpull/solgo/parser" + "github.com/txpull/solgo/syntaxerrors" + "go.uber.org/zap" ) // SolGo is a struct that encapsulates the functionality for parsing and analyzing Solidity contracts. @@ -22,12 +24,12 @@ type SolGo struct { // tokenStream is the stream of tokens produced by the lexer. tokenStream *antlr.CommonTokenStream // solidityParser is the Solidity parser which parses the token stream. - solidityParser *parser.SolidityParser + solidityParser *syntaxerrors.ContextualParser // listeners is a map of listener names to ParseTreeListener instances. // These listeners are invoked as the parser walks the parse tree. listeners listeners // errListener is a SyntaxErrorListener which collects syntax errors encountered during parsing. - errListener *SyntaxErrorListener + errListener *syntaxerrors.SyntaxErrorListener } // New creates a new instance of SolGo. @@ -43,7 +45,7 @@ func New(ctx context.Context, input io.Reader) (*SolGo, error) { inputStream := antlr.NewInputStream(string(ib)) // Create a new SyntaxErrorListener - errListener := NewSyntaxErrorListener() + errListener := syntaxerrors.NewSyntaxErrorListener() // Create a new Solidity lexer with the input stream lexer := parser.NewSolidityLexer(inputStream) @@ -57,14 +59,8 @@ func New(ctx context.Context, input io.Reader) (*SolGo, error) { // Create a new token stream from the lexer stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) - // Create a new Solidity parser with the token stream - solidityParser := parser.NewSolidityParser(stream) - - // Remove the default error listeners - solidityParser.RemoveErrorListeners() - - // Add our SyntaxErrorListener - solidityParser.AddErrorListener(errListener) + // Create a new ContextualParser with the token stream and listener + contextualParser := syntaxerrors.NewContextualParser(stream, errListener) return &SolGo{ ctx: ctx, @@ -72,7 +68,7 @@ func New(ctx context.Context, input io.Reader) (*SolGo, error) { inputStream: inputStream, lexer: lexer, tokenStream: stream, - solidityParser: solidityParser, + solidityParser: contextualParser, errListener: errListener, listeners: make(listeners), }, nil @@ -100,6 +96,11 @@ func (s *SolGo) GetTokenStream() *antlr.CommonTokenStream { // GetParser returns the Solidity parser which parses the token stream. func (s *SolGo) GetParser() *parser.SolidityParser { + return s.solidityParser.SolidityParser +} + +// GetContextualParser returns the ContextualParser which wraps the Solidity parser. +func (s *SolGo) GetContextualParser() *syntaxerrors.ContextualParser { return s.solidityParser } @@ -110,11 +111,15 @@ func (s *SolGo) GetTree() antlr.ParseTree { // Parse initiates the parsing process. It walks the parse tree with all registered listeners // and returns any syntax errors that were encountered during parsing. -func (s *SolGo) Parse() []SyntaxError { +func (s *SolGo) Parse() []syntaxerrors.SyntaxError { tree := s.GetTree() // Walk the parse tree with all registered listeners - for _, listener := range s.GetAllListeners() { + for name, listener := range s.GetAllListeners() { + zap.L().Debug( + "walking parse tree", + zap.String("listener", name.String()), + ) antlr.ParseTreeWalkerDefault.Walk(listener, tree) } diff --git a/solgo_test.go b/solgo_test.go index c351e696..4cfa5d9f 100644 --- a/solgo_test.go +++ b/solgo_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/txpull/solgo/syntaxerrors" + "github.com/txpull/solgo/tests" ) func TestNew(t *testing.T) { @@ -94,3 +96,74 @@ func TestGetInput(t *testing.T) { assert.Equal(t, input, solgo.GetInput(), "input reader is not returned correctly") } + +func TestNew_SyntaxErrors(t *testing.T) { + testCases := []struct { + name string + contract string + expected []syntaxerrors.SyntaxError + }{ + { + name: "Randomly Corrupted Contract", + contract: tests.ReadContractFileForTestFromRootPath(t, "BuggyContract").Content, + expected: []syntaxerrors.SyntaxError{ + { + Line: 9, + Column: 4, + Message: "missing ';' at '}'", + Severity: syntaxerrors.SeverityError, + Context: "SourceUnit", + }, + { + Line: 17, + Column: 12, + Message: "mismatched input '(' expecting {'constant', 'error', 'from', 'global', 'immutable', 'internal', 'override', 'private', 'public', 'revert', Identifier}", + Severity: syntaxerrors.SeverityError, + Context: "SourceUnit", + }, + { + Line: 17, + Column: 27, + Message: "mismatched input ')' expecting {';', '='}", + Severity: syntaxerrors.SeverityError, + Context: "SourceUnit", + }, + { + Line: 18, + Column: 14, + Message: "extraneous input '=' expecting {'constant', 'error', 'from', 'global', 'immutable', 'internal', 'override', 'private', 'public', 'revert', Identifier}", + Severity: syntaxerrors.SeverityError, + Context: "SourceUnit", + }, + { + Line: 24, + Column: 4, + Message: "missing ';' at '}'", + Severity: syntaxerrors.SeverityError, + Context: "SourceUnit", + }, + { + Line: 25, + Column: 0, + Message: "extraneous input '}' expecting {, 'abstract', 'address', 'bool', 'bytes', 'contract', 'enum', 'error', Fixed, FixedBytes, 'from', Function, 'global', 'import', 'interface', 'library', 'mapping', 'pragma', 'revert', SignedIntegerType, 'string', 'struct', 'type', Ufixed, UnsignedIntegerType, 'using', Identifier}", + Severity: syntaxerrors.SeverityError, + Context: "SourceUnit", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a new SolGo instance + solGo, err := New(context.Background(), strings.NewReader(tc.contract)) + assert.NoError(t, err) + assert.NotNil(t, solGo) + + syntaxErrors := solGo.Parse() + + // Check that the syntax errors match the expected syntax errors + assert.Equal(t, tc.expected, syntaxErrors) + }) + } +} diff --git a/syntax_errors.go b/syntax_errors.go deleted file mode 100644 index fdc4b080..00000000 --- a/syntax_errors.go +++ /dev/null @@ -1,85 +0,0 @@ -package solgo - -import ( - "strings" - - "github.com/antlr4-go/antlr/v4" -) - -// SeverityLevel represents the severity of a syntax error. -type SeverityLevel int - -const ( - // SeverityHigh represents a high severity error. - SeverityHigh SeverityLevel = iota - // SeverityMedium represents a medium severity error. - SeverityMedium - // SeverityLow represents a low severity error. - SeverityLow -) - -// SyntaxError represents a syntax error in a Solidity contract. -type SyntaxError struct { - // Line is the line number where the error occurred. - Line int - // Column is the column number where the error occurred. - Column int - // Message is the error message. - Message string - // Severity is the severity level of the error. - Severity SeverityLevel - // Context is the context in which the error occurred. - Context string -} - -// SyntaxErrorListener is a listener for syntax errors in Solidity contracts. -// It extends the DefaultErrorListener from the ANTLR4 parser. -type SyntaxErrorListener struct { - // DefaultErrorListener is the base error listener from the ANTLR4 parser. - *antlr.DefaultErrorListener - // Errors is a slice of SyntaxErrors. - Errors []SyntaxError -} - -// NewSyntaxErrorListener creates a new SyntaxErrorListener. -func NewSyntaxErrorListener() *SyntaxErrorListener { - return &SyntaxErrorListener{ - DefaultErrorListener: antlr.NewDefaultErrorListener(), - Errors: []SyntaxError{}, - } -} - -// SyntaxError is called when a syntax error is encountered. -// It creates a SyntaxError with the line number, column number, error message, severity level, and context, -// and adds it to the Errors slice. -func (s *SyntaxErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) { - context := "" - if parser, ok := recognizer.(*ContextualSolidityParser); ok { - context = parser.CurrentContext() - } - severity := determineSeverity(msg, context) - s.Errors = append(s.Errors, SyntaxError{ - Line: line, - Column: column, - Message: msg, - Severity: severity, - Context: context, - }) -} - -// determineSeverity determines the severity level of a syntax error based on the error message and context. -// It returns SeverityHigh for missing tokens and no viable alternative errors, and for errors in function declarations. -// It returns SeverityMedium for mismatched input and extraneous input errors, and for errors in variable declarations. -// It returns SeverityLow for all other errors. -func determineSeverity(msg string, context string) SeverityLevel { - // High severity errors - if strings.Contains(msg, "missing") || strings.Contains(msg, "no viable alternative") || context == "FunctionDeclaration" { - return SeverityHigh - } - // Medium severity errors - if strings.Contains(msg, "mismatched") || strings.Contains(msg, "extraneous input") || context == "VariableDeclaration" { - return SeverityMedium - } - // Low severity errors - return SeverityLow -} diff --git a/syntax_errors_test.go b/syntax_errors_test.go deleted file mode 100644 index bb6dcc20..00000000 --- a/syntax_errors_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package solgo - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewSyntaxErrorListener(t *testing.T) { - listener := NewSyntaxErrorListener() - - assert.NotNil(t, listener.DefaultErrorListener, "Expected DefaultErrorListener to be initialized") - assert.NotNil(t, listener.Errors, "Expected Errors slice to be initialized") - assert.Equal(t, 0, len(listener.Errors), "Expected Errors slice to be empty") -} - -func TestSyntaxErrorListener_SyntaxError(t *testing.T) { - listener := NewSyntaxErrorListener() - - listener.SyntaxError(nil, nil, 1, 1, "missing ';'", nil) - - assert.Equal(t, 1, len(listener.Errors), "Expected one error to be recorded") - assert.Equal(t, 1, listener.Errors[0].Line, "Expected error line to be 1") - assert.Equal(t, 1, listener.Errors[0].Column, "Expected error column to be 1") - assert.Equal(t, "missing ';'", listener.Errors[0].Message, "Expected error message to be 'missing ';''") - assert.Equal(t, SeverityHigh, listener.Errors[0].Severity, "Expected error severity to be SeverityHigh") - assert.Equal(t, "", listener.Errors[0].Context, "Expected error context to be an empty string") -} - -func TestDetermineSeverity(t *testing.T) { - assert.Equal(t, SeverityHigh, determineSeverity("missing ';'", "FunctionDeclaration"), "Expected severity to be SeverityHigh for missing token in FunctionDeclaration context") - assert.Equal(t, SeverityMedium, determineSeverity("mismatched input", "VariableDeclaration"), "Expected severity to be SeverityMedium for mismatched input in VariableDeclaration context") - assert.Equal(t, SeverityMedium, determineSeverity("extraneous input", "Expression"), "Expected severity to be SeverityLow for extraneous input in Expression context") -} diff --git a/syntaxerrors/contextual_parser.go b/syntaxerrors/contextual_parser.go new file mode 100644 index 00000000..196554c8 --- /dev/null +++ b/syntaxerrors/contextual_parser.go @@ -0,0 +1,32 @@ +package syntaxerrors + +import ( + "github.com/antlr4-go/antlr/v4" + "github.com/txpull/solgo/parser" +) + +// ContextualParser is a wrapper around the SolidityParser that maintains a stack of contexts. +// It provides methods for parsing function and contract definitions, and adds the current context to the SyntaxErrorListener. +type ContextualParser struct { + *parser.SolidityParser + SyntaxErrorListener *SyntaxErrorListener +} + +// NewContextualParser creates a new ContextualSolidityParser. +// It takes a token stream and a SyntaxErrorListener, and returns a pointer to a ContextualSolidityParser. +func NewContextualParser(tokens antlr.TokenStream, listener *SyntaxErrorListener) *ContextualParser { + parser := parser.NewSolidityParser(tokens) + parser.RemoveErrorListeners() + parser.AddErrorListener(listener) + return &ContextualParser{ + SolidityParser: parser, + SyntaxErrorListener: listener, + } +} + +// SourceUnit parses a function definition and adds the "SourceUnit" context to the SyntaxErrorListener. +func (p *ContextualParser) SourceUnit() parser.ISourceUnitContext { + p.SyntaxErrorListener.PushContext("SourceUnit") + defer p.SyntaxErrorListener.PopContext() + return p.SolidityParser.SourceUnit() // Call the original method +} diff --git a/syntaxerrors/doc.go b/syntaxerrors/doc.go new file mode 100644 index 00000000..dc109d19 --- /dev/null +++ b/syntaxerrors/doc.go @@ -0,0 +1,2 @@ +// Package syntaxerrors provides tools for detecting and handling syntax errors in Solidity contracts. +package syntaxerrors diff --git a/syntaxerrors/helpers.go b/syntaxerrors/helpers.go new file mode 100644 index 00000000..be2b2213 --- /dev/null +++ b/syntaxerrors/helpers.go @@ -0,0 +1,9 @@ +package syntaxerrors + +import "strings" + +// ReplaceErrorMessage replaces a specific error message with a new message. +// It takes the original message, the old text to replace, and the new text, and returns the modified message. +func ReplaceErrorMessage(originalMsg, oldText, newText string) string { + return strings.ReplaceAll(originalMsg, oldText, newText) +} diff --git a/syntaxerrors/severity.go b/syntaxerrors/severity.go new file mode 100644 index 00000000..997f6e52 --- /dev/null +++ b/syntaxerrors/severity.go @@ -0,0 +1,65 @@ +package syntaxerrors + +import "strings" + +// SeverityLevel represents the severity of a syntax error. +type SeverityLevel int + +const ( + // SeverityInfo represents a syntax error of informational level. + SeverityInfo SeverityLevel = iota + + // SeverityWarning represents a syntax error of warning level. + SeverityWarning + + // SeverityError represents a syntax error of error level. + SeverityError +) + +// String returns a string representation of the SeverityLevel. +func (s SeverityLevel) String() string { + switch s { + case SeverityInfo: + return "info" + case SeverityWarning: + return "warning" + case SeverityError: + return "error" + default: + return "unknown" + } +} + +var ( + // severityMap maps error messages to their severity level. + severityMap = map[string]SeverityLevel{ + "missing": SeverityError, + "mismatched": SeverityError, + "no viable alternative": SeverityError, + "extraneous input": SeverityError, + "cannot find symbol": SeverityWarning, + "method not found": SeverityWarning, + } +) + +// DetermineSeverity determines the severity of a syntax error. +// It returns the severity of the error. +func (l *SyntaxErrorListener) DetermineSeverity(msg, context string) SeverityLevel { + // Replace the error message if needed + msg = ReplaceErrorMessage(msg, "missing Semicolon at '}'", "missing ';' at '}'") + + // Check if the error message is in the map + for err, severity := range severityMap { + if strings.Contains(msg, err) { + return severity + } + } + + // Context-specific rules + if context == "FunctionDeclaration" && strings.Contains(msg, "expected") { + return SeverityError + } + + // Default to low severity + return SeverityInfo +} diff --git a/syntaxerrors/syntax_errors.go b/syntaxerrors/syntax_errors.go new file mode 100644 index 00000000..8036db76 --- /dev/null +++ b/syntaxerrors/syntax_errors.go @@ -0,0 +1,83 @@ +package syntaxerrors + +import ( + "fmt" + + "github.com/antlr4-go/antlr/v4" + "github.com/txpull/solgo/parser" +) + +// SyntaxError represents a syntax error in a Solidity contract. +// It includes the line and column where the error occurred, the error message, the severity of the error, and the context in which the error occurred. +type SyntaxError struct { + Line int + Column int + Message string + Severity SeverityLevel + Context string +} + +// Error returns the error message. +func (e SyntaxError) Error() error { + return fmt.Errorf("syntax error: %s at line %d, column %d in context '%s'. Severity: %s", e.Message, e.Line, e.Column, e.Context, e.Severity.String()) +} + +// SyntaxErrorListener is a listener for syntax errors in Solidity contracts. +// It maintains a stack of contexts and a slice of SyntaxErrors. +type SyntaxErrorListener struct { + *parser.BaseSolidityParserListener + *antlr.DefaultErrorListener + Errors []SyntaxError + contexts []string +} + +// NewSyntaxErrorListener creates a new SyntaxErrorListener. +// It returns a pointer to a SyntaxErrorListener with an empty slice of SyntaxErrors and an empty stack of contexts. +func NewSyntaxErrorListener() *SyntaxErrorListener { + return &SyntaxErrorListener{ + DefaultErrorListener: antlr.NewDefaultErrorListener(), + Errors: []SyntaxError{}, + } +} + +// PushContext adds a context to the stack. +func (l *SyntaxErrorListener) PushContext(ctx string) { + l.contexts = append(l.contexts, ctx) +} + +// PopContext removes the most recent context from the stack. +func (l *SyntaxErrorListener) PopContext() { + if len(l.contexts) > 0 { + // Remove the last context + l.contexts = l.contexts[:len(l.contexts)-1] + } +} + +// SyntaxError handles a syntax error. +// It creates a new SyntaxError with the given parameters and adds it to the Errors slice. +func (l *SyntaxErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) { + // Replace the error message if needed + msg = ReplaceErrorMessage(msg, "Semicolon", "';'") + + // Create a new SyntaxError + err := SyntaxError{ + Line: line, + Column: column, + Message: msg, + Severity: l.DetermineSeverity(msg, l.currentContext()), + Context: l.currentContext(), + } + + // Add the error to the Errors slice + l.Errors = append(l.Errors, err) +} + +func (l *SyntaxErrorListener) currentContext() string { + // If there are no contexts, return an empty string + if len(l.contexts) == 0 { + return "" + } + + // Return the current context (the last one in the slice) + return l.contexts[len(l.contexts)-1] +} diff --git a/syntaxerrors/syntax_errors_test.go b/syntaxerrors/syntax_errors_test.go new file mode 100644 index 00000000..b19bd1d1 --- /dev/null +++ b/syntaxerrors/syntax_errors_test.go @@ -0,0 +1,92 @@ +package syntaxerrors + +import ( + "testing" + + "github.com/antlr4-go/antlr/v4" + "github.com/stretchr/testify/assert" + "github.com/txpull/solgo/parser" + "github.com/txpull/solgo/tests" +) + +func TestSyntaxErrorListener(t *testing.T) { + testCases := []struct { + name string + contract string + expected []SyntaxError + }{ + { + name: "Randomly Corrupted Contract", + contract: tests.ReadContractFileForTest(t, "BuggyContract").Content, + expected: []SyntaxError{ + { + Line: 9, + Column: 4, + Message: "missing ';' at '}'", + Severity: SeverityError, + Context: "SourceUnit", + }, + { + Line: 17, + Column: 12, + Message: "mismatched input '(' expecting {'constant', 'error', 'from', 'global', 'immutable', 'internal', 'override', 'private', 'public', 'revert', Identifier}", + Severity: SeverityError, + Context: "SourceUnit", + }, + { + Line: 17, + Column: 27, + Message: "mismatched input ')' expecting {';', '='}", + Severity: SeverityError, + Context: "SourceUnit", + }, + { + Line: 18, + Column: 14, + Message: "extraneous input '=' expecting {'constant', 'error', 'from', 'global', 'immutable', 'internal', 'override', 'private', 'public', 'revert', Identifier}", + Severity: SeverityError, + Context: "SourceUnit", + }, + { + Line: 24, + Column: 4, + Message: "missing ';' at '}'", + Severity: SeverityError, + Context: "SourceUnit", + }, + { + Line: 25, + Column: 0, + Message: "extraneous input '}' expecting {, 'abstract', 'address', 'bool', 'bytes', 'contract', 'enum', 'error', Fixed, FixedBytes, 'from', Function, 'global', 'import', 'interface', 'library', 'mapping', 'pragma', 'revert', SignedIntegerType, 'string', 'struct', 'type', Ufixed, UnsignedIntegerType, 'using', Identifier}", + Severity: SeverityError, + Context: "SourceUnit", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create an ANTLR input stream from the contract string + input := antlr.NewInputStream(tc.contract) + + // Create a Solidity lexer + lexer := parser.NewSolidityLexer(input) + + // Create an ANTLR token stream from the lexer + tokens := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) + + // Create a new SyntaxErrorListener + listener := NewSyntaxErrorListener() + + // Create a ContextualParser with the token stream and listener + parser := NewContextualParser(tokens, listener) + + // Parse the contract + parser.SourceUnit() + + // Check that the errors match the expected errors + assert.Equal(t, tc.expected, listener.Errors) + }) + } +} diff --git a/tests/helpers.go b/tests/helpers.go index b2117823..efa250e5 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -1,6 +1,7 @@ package tests import ( + "fmt" "os" "path/filepath" "testing" @@ -30,3 +31,22 @@ func ReadContractFileForTest(t *testing.T, name string) TestContract { return TestContract{Path: path, Content: string(content)} } + +// ReadContractFileForTestFromRootPath reads a contract file for testing from root of the solgo project +func ReadContractFileForTestFromRootPath(t *testing.T, name string) TestContract { + dir, err := os.Getwd() + assert.NoError(t, err) + + fmt.Println(dir) + + contractsDir := filepath.Join(dir, "data", "tests") + path := filepath.Join(contractsDir, name+".sol") + + _, err = os.Stat(contractsDir) + assert.NoError(t, err) + + content, err := os.ReadFile(path) + assert.NoError(t, err) + + return TestContract{Path: path, Content: string(content)} +}