diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a5082e..66301f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,4 +25,4 @@ jobs: steps: - uses: actions/checkout@v3 - name: test - run: go test ./... -v + run: go test -race -cover -shuffle=on ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..808b277 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Test coverage file +cover.html + +# Go workspace file +go.work + +# Dependency directory +/vendor/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6b5d723 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: help coverage +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +coverage: ## generate test coverage profile to cover.html + go test -cover -coverprofile=cover.out ./... + go tool cover -html=cover.out -o cover.html diff --git a/ast/ast.go b/ast/ast.go index a869c6a..a58ca8a 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -1,113 +1,355 @@ package ast -import "github.com/uji/solparser/token" +import ( + "strings" + "github.com/uji/solparser/token" +) + +// All node types implement the Node interface. type Node interface { Pos() token.Pos End() token.Pos } -type PragmaValue struct { - Version string - Expression string +// All contract-body-element node types implement the ContractBodyElement interface. +type ContractBodyElement interface { + Node + contractBodyElementNode() } -type PragmaDirective struct { - PragmaName string - PragmaValue PragmaValue +// All type-name node types implement the TypeName interface. +type TypeName interface { + Node + typeNameNode() } -type FunctionDescriptor struct { - Name string +// All expression node types implement the Expression interface. +type Expression interface { + Node + expressionNode() } -type StateMutability struct { - Pure bool +// All literal node types implement the Literal interface. +type Literal interface { + Node + literalNode() + expressionNode() } -type ModifierList struct { - StateMutability *StateMutability +// All statement node types implement the Statement interface. +type Statement interface { + Node + statementNode() +} + +// ---------------------------------------------------------------------------- +// ImportDirectiveElement Nodes + +// All import-directive-element node (own definition for solparser) types implement the ImportDirectiveElement interface. +type ImportDirectiveElement interface { + Node + importDirectiveElementNode() +} + +type ImportDirectivePathElement struct { + Path Path + As *token.Pos + Identifier *Identifier +} + +func (i ImportDirectivePathElement) Pos() token.Pos { return i.Path.Pos() } +func (i ImportDirectivePathElement) End() token.Pos { return i.Identifier.End() } + +type ImportDirectiveSymbolAliasesElement struct { + SymbolAliases *SymbolAliases + From token.Pos + Path Path +} + +func (i ImportDirectiveSymbolAliasesElement) Pos() token.Pos { return i.SymbolAliases.Pos() } +func (i ImportDirectiveSymbolAliasesElement) End() token.Pos { return i.Path.End() } + +type ImportDirectiveMulElement struct { + Mul token.Pos + As token.Pos + Identifier Identifier + From token.Pos + Path Path +} + +func (i ImportDirectiveMulElement) Pos() token.Pos { return i.Mul } +func (i ImportDirectiveMulElement) End() token.Pos { return i.Path.End() } + +func (i *ImportDirectivePathElement) importDirectiveElementNode() {} +func (i *ImportDirectiveSymbolAliasesElement) importDirectiveElementNode() {} +func (i *ImportDirectiveMulElement) importDirectiveElementNode() {} + +// ---------------------------------------------------------------------------- + +type ImportDirective struct { + Import token.Pos + Element ImportDirectiveElement + Semicolon token.Pos } -type TypeName struct { - ElementalyTypeName string +func (i ImportDirective) Pos() token.Pos { return i.Import } +func (i ImportDirective) End() token.Pos { return i.Semicolon } + +type Path token.Token + +func (p Path) Pos() token.Pos { return p.Position } +func (p Path) End() token.Pos { + return token.Pos{ + Column: p.Position.Column + len(p.Value), // Path does not include newline code. + Line: p.Position.Line, + } } -type EventParameter struct { - TypeName *TypeName +type SymbolAlias struct { + Symbol Identifier + As *token.Pos + Alias *Identifier } -type ParameterList struct { - EventParameter *EventParameter +func (s SymbolAlias) Pos() token.Pos { return s.Symbol.Pos() } +func (s SymbolAlias) End() token.Pos { + if s.Alias != nil { + return s.Alias.End() + } + return s.Symbol.End() } -type ReturnParameters struct { - ParameterList *ParameterList +type SymbolAliases struct { + LBrace token.Pos + Aliases []*SymbolAlias + Commas []*token.Pos + RBrace token.Pos } +func (s SymbolAliases) Pos() token.Pos { return s.LBrace } +func (s SymbolAliases) End() token.Pos { return s.RBrace } + +type PragmaDirective struct { + Pragma token.Pos + PragmaTokens []*token.Token + Semicolon token.Pos +} + +func (p PragmaDirective) Pos() token.Pos { return p.Pragma } +func (p PragmaDirective) End() token.Pos { return p.Semicolon } + +type ModifierList struct { + Visibility *Visibility + StateMutability *StateMutability +} + +type Visibility = token.Token + +// Parameter is type of ParameterList elements +type Parameter struct { + TypeName TypeName +} + +type ParameterList []*Parameter + +type StateMutability = token.Token + +// identifier | fallback | recevie +type FunctionDescriptor = token.Token + +type FunctionDefinitionReturns struct { + From token.Pos + LParen token.Pos + ParameterList ParameterList + RParen token.Pos +} + +// ---------------------------------------------------------------------------- +// ContractBodyElement Nodes + type FunctionDefinition struct { - FunctionDescriptor *FunctionDescriptor + From token.Pos + FunctionDescriptor FunctionDescriptor + LParen token.Pos + RParen token.Pos ModifierList *ModifierList - ReturnParameters *ReturnParameters + Returns *FunctionDefinitionReturns + Block *Block } +func (f FunctionDefinition) Pos() token.Pos { return f.From } +func (f FunctionDefinition) End() token.Pos { return f.Block.End() } + +func (f *FunctionDefinition) contractBodyElementNode() {} + +// ---------------------------------------------------------------------------- + type InheritanceSpecifier struct { - IdentifierPath string - CallArgumentList CallArgumentList + IdentifierPath IdentifierPath + CallArgumentList *CallArgumentList +} + +// ---------------------------------------------------------------------------- +// ImportDirectiveElement Nodes + +// All call-argument-list-elements node (own definition for solparser) types implement the CallArgumentListElements interface. +type CallArgumentListElements interface { + Node + callArgumentListElementsNode() } -type CallArgumentList struct{} +type CallArgumentListExpretion struct { + Expression Expression + Comma *token.Pos +} -type ContractBodyElement Node +func (c CallArgumentListExpretion) Pos() token.Pos { return c.Expression.Pos() } +func (c CallArgumentListExpretion) End() token.Pos { + if c.Comma != nil { + return *c.Comma + } + return c.Expression.End() +} + +type CallArgumentListExpretions []*CallArgumentListExpretion + +func (c CallArgumentListExpretions) Pos() token.Pos { return c[0].Pos() } +func (c CallArgumentListExpretions) End() token.Pos { return c[len(c)-1].End() } +func (c CallArgumentListExpretions) callArgumentListElementsNode() {} + +type CallArgumentListNamedExpretion struct { + Identifier Identifier + Colon token.Pos + Expression Expression + Comma *token.Pos +} + +func (c CallArgumentListNamedExpretion) Pos() token.Pos { return c.Identifier.Pos() } +func (c CallArgumentListNamedExpretion) End() token.Pos { + if c.Comma != nil { + return *c.Comma + } + return c.Expression.End() +} + +type CallArgumentListNamedExpretions struct { + LBrace token.Pos + NamedExpretions []*CallArgumentListNamedExpretion + RBrace token.Pos +} + +func (c CallArgumentListNamedExpretions) Pos() token.Pos { return c.LBrace } +func (c CallArgumentListNamedExpretions) End() token.Pos { return c.RBrace } +func (c *CallArgumentListNamedExpretions) callArgumentListElementsNode() {} + +// ---------------------------------------------------------------------------- + +type CallArgumentList struct { + LParen token.Pos + Elements CallArgumentListElements + RParen token.Pos +} + +type IdentifierPathElement struct { + Identifier Identifier + Period *token.Pos +} +type IdentifierPath struct { + Elements []*IdentifierPathElement +} + +func (i IdentifierPath) Pos() token.Pos { return i.Elements[0].Identifier.Pos() } +func (i IdentifierPath) End() token.Pos { return i.Elements[len(i.Elements)-1].Identifier.End() } type ContractDefinition struct { - Position token.Pos - Abstract bool - InheritanceSpecifiers []*InheritanceSpecifier - ContractBodyElements []*ContractBodyElement + Abstract *token.Pos + Contract token.Pos + Identifier Identifier + LBrace token.Pos + ContractBodyElements []ContractBodyElement + RBrace token.Pos } // A File node represents a Solidity source file. type SourceUnit struct { PragmaDirective *PragmaDirective + ImportDirective *ImportDirective ContractDefinition *ContractDefinition FunctionDefinition *FunctionDefinition } // ---------------------------------------------------------------------------- -// Expression Nodes +// TypeName Nodes -type Expression interface { - Node - expressionNode() +type ElementaryTypeName []*token.Token + +func (e ElementaryTypeName) Pos() token.Pos { + return e[0].Position +} + +func (e ElementaryTypeName) End() token.Pos { + eTkn := e[len(e)-1] + return token.Pos{ + Column: eTkn.Position.Column + len(eTkn.Value), + Line: eTkn.Position.Line, + } } +func (e ElementaryTypeName) typeNameNode() {} + // ---------------------------------------------------------------------------- -// Literal Nodes +// Expression Nodes -type Literal interface { - Node - literalNode() - expressionNode() +type Identifier token.Token + +func (i Identifier) Pos() token.Pos { return i.Position } +func (i Identifier) End() token.Pos { + return token.Pos{ + Column: i.Position.Column + len(i.Value), + Line: i.Position.Line, + } } +func (i *Identifier) expressionNode() {} + +// ---------------------------------------------------------------------------- +// Literal Nodes + type BooleanLiteral struct { Token token.Token } func (b *BooleanLiteral) Pos() token.Pos { - return b.Token.Pos + return b.Token.Position } func (b *BooleanLiteral) End() token.Pos { return token.Pos{ - Column: b.Token.Pos.Column + len(b.Token.Text), - Line: b.Token.Pos.Line, + Column: b.Token.Position.Column + len(b.Token.Value), + Line: b.Token.Position.Line, } } -type StringLiteral struct { - List []Node // EmptyStringLiteral | NonEmptyStringLiteral +// EmptyStringLiteral | NonEmptyStringLiteral +type StringLiteral token.Token + +func (s StringLiteral) Pos() token.Pos { + return s.Position +} + +func (s StringLiteral) End() (pos token.Pos) { + // Calculate Line and Offset by referring to the number of new line codes + nc := strings.Count(s.Value, "\n") + + pos.Line = s.Position.Line + nc + if nc == 0 { + pos.Column = s.Position.Column + len(s.Value) - 1 + return + } + + pos.Column = len(s.Value) - strings.LastIndexByte(s.Value, '\n') - 1 + return } type HexStringLiteral []*HexString @@ -222,37 +464,32 @@ type HexString struct { String string } -type ElementaryTypeNameKind int +// ---------------------------------------------------------------------------- -const ( - ElementaryTypeName_Address ElementaryTypeNameKind = iota + 1 - ElementaryTypeName_AddressPayable - ElementaryTypeName_Bool - ElementaryTypeName_String - ElementaryTypeName_Bytes - ElementaryTypeName_SignedIntegerType - ElementaryTypeName_UnsignedIntegerType - ElementaryTypeName_FixedBytes - ElementaryTypeName_Fixed - ElementaryTypeName_Ufixed -) +type Block struct { + LBracePos token.Pos + RBracePos token.Pos + Nodes []Node // statement | unblocked-block +} -type ElementaryTypeName struct { - Pos token.Pos - Kind ElementaryTypeNameKind +func (b Block) Pos() token.Pos { + return b.LBracePos +} + +func (b Block) End() token.Pos { + return b.RBracePos } +// ---------------------------------------------------------------------------- +// Statement Nodes + type ReturnStatement struct { - Return token.Pos + From token.Pos + SemiPos token.Pos Expression Expression } -func (r *ReturnStatement) Pos() token.Pos { - return r.Return -} -func (r *ReturnStatement) End() token.Pos { - return token.Pos{ - Column: r.Expression.End().Column + 1, - Line: r.Expression.End().Line, - } -} +func (r ReturnStatement) Pos() token.Pos { return r.From } +func (r ReturnStatement) End() token.Pos { return r.SemiPos } + +func (s *ReturnStatement) statementNode() {} diff --git a/ast/ast_test.go b/ast/ast_test.go index 1deb8e0..6241b3d 100644 --- a/ast/ast_test.go +++ b/ast/ast_test.go @@ -8,6 +8,24 @@ import ( "github.com/uji/solparser/token" ) +var ( + _ ast.ImportDirectiveElement = &ast.ImportDirectivePathElement{} + _ ast.ImportDirectiveElement = &ast.ImportDirectiveSymbolAliasesElement{} + _ ast.ImportDirectiveElement = &ast.ImportDirectiveMulElement{} + _ ast.Node = &ast.ImportDirective{} + _ ast.Node = &ast.Path{} + _ ast.Node = &ast.SymbolAlias{} + _ ast.Node = &ast.SymbolAliases{} + _ ast.Node = &ast.PragmaDirective{} + _ ast.ContractBodyElement = &ast.FunctionDefinition{} + _ ast.TypeName = ast.ElementaryTypeName{} + _ ast.Expression = &ast.Identifier{} + _ ast.Literal = &ast.BooleanLiteral{} + _ ast.Literal = &ast.StringLiteral{} + _ ast.Node = &ast.Block{} + _ ast.Statement = &ast.ReturnStatement{} +) + func TestNode_End(t *testing.T) { tests := []struct { name string @@ -56,6 +74,36 @@ func TestNode_End(t *testing.T) { Line: 3, }, }, + { + name: "StringLiteral", + node: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Value: "\"Hello world!!\"", + Position: token.Pos{ + Column: 4, + Line: 3, + }, + }, + exptEnd: token.Pos{ + Column: 18, + Line: 3, + }, + }, + { + name: "StringLiteral with new line codes", + node: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Value: "\"Hello \n new \nworld!!\"", + Position: token.Pos{ + Column: 4, + Line: 3, + }, + }, + exptEnd: token.Pos{ + Column: 8, + Line: 5, + }, + }, } for _, tt := range tests { diff --git a/block.go b/block.go new file mode 100644 index 0000000..2166456 --- /dev/null +++ b/block.go @@ -0,0 +1,37 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseBlock() (*ast.Block, error) { + lblace, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + if lblace.Type != token.LBrace { + return nil, token.NewPosError(lblace.Position, "not found LBrace.") + } + + stmt, err := p.ParseStatement() + if err != nil { + return nil, err + } + + rblace, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + if rblace.Type != token.RBrace { + return nil, token.NewPosError(rblace.Position, "not found RBrace.") + } + + return &ast.Block{ + LBracePos: lblace.Position, + RBracePos: rblace.Position, + Nodes: []ast.Node{stmt}, + }, nil +} diff --git a/block_test.go b/block_test.go new file mode 100644 index 0000000..403991d --- /dev/null +++ b/block_test.go @@ -0,0 +1,78 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseBlock(t *testing.T) { + tests := []struct { + name string + input string + want *ast.Block + err *token.PosError + }{ + { + name: "One Statement", + input: "{ \nreturn \"Hello World!!\";\n }", + want: &ast.Block{ + LBracePos: token.Pos{Column: 1, Line: 1}, + RBracePos: token.Pos{Column: 2, Line: 3}, + Nodes: []ast.Node{ + &ast.ReturnStatement{ + From: token.Pos{Column: 1, Line: 2}, + SemiPos: token.Pos{Column: 23, Line: 2}, + Expression: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Position: token.Pos{Column: 8, Line: 2}, + Value: "\"Hello World!!\"", + }, + }, + }, + }, + err: nil, + }, + { + name: "Not found LBrace", + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{Column: 1, Line: 1}, + Msg: "not found LBrace.", + }, + }, + { + name: "Not found RBrace", + input: "{ \nreturn \"Hello World!!\";", + err: &token.PosError{ + Pos: token.Pos{Column: 24, Line: 2}, + Msg: "not found RBrace.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseBlock() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} diff --git a/callArgumentList.go b/callArgumentList.go new file mode 100644 index 0000000..c538609 --- /dev/null +++ b/callArgumentList.go @@ -0,0 +1,152 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseCallArgumentList() (*ast.CallArgumentList, error) { + lparen, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + tkn, err := p.lexer.Peek() + if err != nil { + return nil, err + } + if tkn.Type == token.RParen { + p.lexer.Scan() + return &ast.CallArgumentList{ + LParen: lparen.Position, + RParen: tkn.Position, + }, nil + } + + var elements ast.CallArgumentListElements + if tkn.Type == token.LBrace { + es, err := p.ParseCallArgumentListExpretions() + if err != nil { + return nil, err + } + elements = es + } else { + nes, err := p.ParseCallArgumentListNamedExpretions() + if err != nil { + return nil, err + } + elements = nes + } + + rparen, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if rparen.Type != token.RParen { + return nil, token.NewPosError(rparen.Position, "not found RParen.") + } + + return &ast.CallArgumentList{ + LParen: lparen.Position, + Elements: elements, + RParen: rparen.Position, + }, nil +} + +func (p *Parser) ParseCallArgumentListExpretions() (ast.CallArgumentListExpretions, error) { + ex, err := p.ParseExpression() + if err != nil { + return nil, err + } + + var exs ast.CallArgumentListExpretions + for { + cmm, err := p.lexer.Peek() + if err != nil || cmm.Type != token.Comma { + break + } + p.lexer.Scan() + + exs = append(exs, &ast.CallArgumentListExpretion{ + Expression: ex, + Comma: &cmm.Position, + }) + + e, err := p.ParseExpression() + ex = e + } + + exs = append(exs, &ast.CallArgumentListExpretion{ + Expression: ex, + }) + + return exs, nil +} + +func (p *Parser) ParseCallArgumentListNamedExpretions() (*ast.CallArgumentListNamedExpretions, error) { + lbrace, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + exs := make([]*ast.CallArgumentListNamedExpretion, 0, 1) + + for { + id, err := p.lexer.Peek() + if err != nil { + return nil, err + } + + if !isIdentifier(id) { + return nil, token.NewPosError(id.Position, "not found identifier.") + } + p.lexer.Scan() + + cln, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if cln.Type != token.Colon { + return nil, token.NewPosError(cln.Position, "not found Colon.") + } + + ex, err := p.ParseExpression() + if err != nil { + return nil, err + } + + cmm, err := p.lexer.Peek() + if err != nil { + return nil, err + } + if cmm.Type != token.Comma { + exs = append(exs, &ast.CallArgumentListNamedExpretion{ + Identifier: ast.Identifier(id), + Colon: cln.Position, + Expression: ex, + }) + break + } + p.lexer.Scan() + exs = append(exs, &ast.CallArgumentListNamedExpretion{ + Identifier: ast.Identifier(id), + Colon: cln.Position, + Expression: ex, + Comma: &cmm.Position, + }) + } + + rbrace, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if rbrace.Type != token.RBrace { + return nil, token.NewPosError(rbrace.Position, "not found RBrace.") + } + + return &ast.CallArgumentListNamedExpretions{ + RBrace: rbrace.Position, + NamedExpretions: exs, + LBrace: lbrace.Position, + }, nil +} diff --git a/contractDefinition.go b/contractDefinition.go index bad1e2b..47db899 100644 --- a/contractDefinition.go +++ b/contractDefinition.go @@ -1,39 +1,60 @@ package solparser import ( - "errors" - "github.com/uji/solparser/ast" "github.com/uji/solparser/token" ) func (p *Parser) ParseContractDefinition() (*ast.ContractDefinition, error) { - var abstract bool - p.lexer.Scan() - tkn := p.lexer.Token() - if tkn.TokenType == token.Abstract { - abstract = true - p.lexer.Scan() + cntr, err := p.lexer.Scan() + if err != nil { + return nil, err + } + var abstractPos *token.Pos + if cntr.Type == token.Abstract { + pos := cntr.Position + abstractPos = &pos + cntr, err = p.lexer.Scan() + if err != nil { + return nil, err + } + } + if cntr.Type != token.Contract { + return nil, token.NewPosError(cntr.Position, "not found contract keyword.") } - if tkn.TokenType != token.Contract { - return nil, errors.New("not found contract definition") + + i, err := p.ParseIdentifier() + if err != nil { + return nil, err } - p.lexer.Scan() - if p.lexer.Token().TokenType != token.Unknown { - return nil, errors.New("not found left brace") + lbrace, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if lbrace.Type != token.LBrace { + return nil, token.NewPosError(lbrace.Position, "not found left brace.") } - if p.lexer.Token().TokenType != token.LBrace { - return nil, errors.New("not found left brace") + fn, err := p.ParseFunctionDefinition() + if err != nil { + return nil, err } - p.lexer.Scan() - if p.lexer.Token().TokenType != token.RBrace { - return nil, errors.New("not found right brace") + rbrace, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if rbrace.Type != token.RBrace { + return nil, token.NewPosError(rbrace.Position, "not found right brace.") } return &ast.ContractDefinition{ - Abstract: abstract, + Abstract: abstractPos, + Contract: cntr.Position, + Identifier: i, + LBrace: lbrace.Position, + ContractBodyElements: []ast.ContractBodyElement{fn}, + RBrace: rbrace.Position, }, nil } diff --git a/contractDefinition_test.go b/contractDefinition_test.go new file mode 100644 index 0000000..3fd6a1c --- /dev/null +++ b/contractDefinition_test.go @@ -0,0 +1,240 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseContractDefinition(t *testing.T) { + tests := []struct { + name string + input string + want *ast.ContractDefinition + err *token.PosError + }{ + { + name: "normal case", + input: `contract HelloWorld { + function hello() public pure returns (string) { + return "Hello World!!"; + } +}`, + want: &ast.ContractDefinition{ + Contract: token.Pos{Column: 1, Line: 1}, + Identifier: ast.Identifier{ + Type: token.Identifier, + Value: "HelloWorld", + Position: token.Pos{Column: 10, Line: 1}, + }, + LBrace: token.Pos{Column: 21, Line: 1}, + ContractBodyElements: []ast.ContractBodyElement{ + &ast.FunctionDefinition{ + From: token.Pos{Column: 5, Line: 2}, + FunctionDescriptor: token.Token{ + Type: token.Identifier, + Value: "hello", + Position: token.Pos{Column: 14, Line: 2}, + }, + LParen: token.Pos{Column: 19, Line: 2}, + RParen: token.Pos{Column: 20, Line: 2}, + ModifierList: &ast.ModifierList{ + Visibility: &token.Token{ + Type: token.Public, + Value: "public", + Position: token.Pos{Column: 22, Line: 2}, + }, + StateMutability: &token.Token{ + Type: token.Pure, + Value: "pure", + Position: token.Pos{Column: 29, Line: 2}, + }, + }, + Returns: &ast.FunctionDefinitionReturns{ + From: token.Pos{Column: 34, Line: 2}, + LParen: token.Pos{Column: 42, Line: 2}, + ParameterList: []*ast.Parameter{ + { + TypeName: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 43, Line: 2}, + }, + }, + }, + }, + RParen: token.Pos{Column: 49, Line: 2}, + }, + Block: &ast.Block{ + LBracePos: token.Pos{Column: 51, Line: 2}, + RBracePos: token.Pos{Column: 5, Line: 4}, + Nodes: []ast.Node{ + &ast.ReturnStatement{ + From: token.Pos{Column: 9, Line: 3}, + SemiPos: token.Pos{Column: 31, Line: 3}, + Expression: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Position: token.Pos{Column: 16, Line: 3}, + Value: "\"Hello World!!\"", + }, + }, + }, + }, + }, + }, + RBrace: token.Pos{Column: 1, Line: 5}, + }, + }, + { + name: "with abstract", + input: `abstract contract HelloWorld { + function hello() public pure returns (string) { + return "Hello World!!"; + } +}`, + want: &ast.ContractDefinition{ + Abstract: &token.Pos{Column: 1, Line: 1}, + Contract: token.Pos{Column: 10, Line: 1}, + Identifier: ast.Identifier{ + Type: token.Identifier, + Value: "HelloWorld", + Position: token.Pos{Column: 19, Line: 1}, + }, + LBrace: token.Pos{Column: 30, Line: 1}, + ContractBodyElements: []ast.ContractBodyElement{ + &ast.FunctionDefinition{ + From: token.Pos{Column: 5, Line: 2}, + FunctionDescriptor: token.Token{ + Type: token.Identifier, + Value: "hello", + Position: token.Pos{Column: 14, Line: 2}, + }, + LParen: token.Pos{Column: 19, Line: 2}, + RParen: token.Pos{Column: 20, Line: 2}, + ModifierList: &ast.ModifierList{ + Visibility: &token.Token{ + Type: token.Public, + Value: "public", + Position: token.Pos{Column: 22, Line: 2}, + }, + StateMutability: &token.Token{ + Type: token.Pure, + Value: "pure", + Position: token.Pos{Column: 29, Line: 2}, + }, + }, + Returns: &ast.FunctionDefinitionReturns{ + From: token.Pos{Column: 34, Line: 2}, + LParen: token.Pos{Column: 42, Line: 2}, + ParameterList: []*ast.Parameter{ + { + TypeName: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 43, Line: 2}, + }, + }, + }, + }, + RParen: token.Pos{Column: 49, Line: 2}, + }, + Block: &ast.Block{ + LBracePos: token.Pos{Column: 51, Line: 2}, + RBracePos: token.Pos{Column: 5, Line: 4}, + Nodes: []ast.Node{ + &ast.ReturnStatement{ + From: token.Pos{Column: 9, Line: 3}, + SemiPos: token.Pos{Column: 31, Line: 3}, + Expression: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Position: token.Pos{Column: 16, Line: 3}, + Value: "\"Hello World!!\"", + }, + }, + }, + }, + }, + }, + RBrace: token.Pos{Column: 1, Line: 5}, + }, + }, + { + name: "not found contract keyword", + input: `HelloWorld { + function hello() public pure returns (string) { + return "Hello World!!"; + } +}`, + err: &token.PosError{ + Pos: token.Pos{Column: 1, Line: 1}, + Msg: "not found contract keyword.", + }, + }, + { + name: "not found identifier", + input: `contract { + function hello() public pure returns (string) { + return "Hello World!!"; + } +}`, + err: &token.PosError{ + Pos: token.Pos{Column: 10, Line: 1}, + Msg: "keyword is not available as identifier.", + }, + }, + { + name: "not found LBrace", + input: "contract HelloWorld function", + err: &token.PosError{ + Pos: token.Pos{Column: 21, Line: 1}, + Msg: "not found left brace.", + }, + }, + { + name: "not found FunctionDefinition", + input: "contract HelloWorld { hello() }", + err: &token.PosError{ + Pos: token.Pos{Column: 23, Line: 1}, + Msg: "not found function keyword.", + }, + }, + { + name: "not found RBrace", + input: `contract HelloWorld { + function hello() public pure returns (string) { + return "Hello World!!"; + }`, + err: &token.PosError{ + Pos: token.Pos{Column: 6, Line: 4}, + Msg: "not found right brace.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseContractDefinition() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} diff --git a/error.go b/error.go deleted file mode 100644 index 9fa3337..0000000 --- a/error.go +++ /dev/null @@ -1,26 +0,0 @@ -package solparser - -import ( - "github.com/uji/solparser/token" -) - -type Error struct { - Pos token.Pos - Msg string -} - -var _ error = &Error{} - -func newError(pos token.Pos, msg string) *Error { - return &Error{ - Pos: pos, - Msg: msg, - } -} - -func (e *Error) Error() string { - if e.Pos.IsValid() { - return e.Pos.String() + ": " + e.Msg - } - return e.Msg -} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..22bdb95 --- /dev/null +++ b/example_test.go @@ -0,0 +1,35 @@ +package solparser_test + +import ( + "fmt" + "strings" + + "github.com/uji/solparser" + "github.com/uji/solparser/ast" +) + +func Example() { + input := + `pragma solidity ^0.8.13; + +contract HelloWorld { + function hello() public pure returns (string) { + return "Hello World!!"; + } +}` + + parser := solparser.New(strings.NewReader(input)) + + got, err := parser.Parse() + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(got.ContractDefinition.Identifier.Value) + fmt.Println(got.ContractDefinition.ContractBodyElements[0].(*ast.FunctionDefinition).FunctionDescriptor.Value) + + // Output: + // HelloWorld + // hello +} diff --git a/expression.go b/expression.go new file mode 100644 index 0000000..5ad23a8 --- /dev/null +++ b/expression.go @@ -0,0 +1,19 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseExpression() (ast.Expression, error) { + tkn, err := p.lexer.Peek() + if err != nil { + return nil, err + } + + if tkn.Type == token.NonEmptyStringLiteral { + return p.ParseLiteral() + } + + return nil, token.NewPosError(tkn.Position, "not found expression.") +} diff --git a/expression_test.go b/expression_test.go new file mode 100644 index 0000000..a9683f5 --- /dev/null +++ b/expression_test.go @@ -0,0 +1,63 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseExpression(t *testing.T) { + tests := []struct { + name string + input string + want ast.Expression + err *token.PosError + }{ + { + name: "Literal", + input: `"Hello World!!";`, + want: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Value: `"Hello World!!"`, + Position: pos(1, 1), + }, + err: nil, + }, + { + name: "Not expression", + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{ + Column: 1, + Line: 1, + }, + Msg: "not found expression.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseExpression() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} diff --git a/functionDefinition.go b/functionDefinition.go index 4268ca6..4c9ca81 100644 --- a/functionDefinition.go +++ b/functionDefinition.go @@ -1,15 +1,163 @@ package solparser -import "github.com/uji/solparser/ast" +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) -func (p *Parser) ParseFunctionDefinition() (*ast.FunctionDefinition, error) { - return nil, nil +func (p *Parser) ParseVisibility() (ast.Visibility, error) { + tkn, err := p.lexer.Scan() + if err != nil { + return token.Token{}, err + } + + switch tkn.Type { + case token.Internal, token.External, token.Public, token.Private: + return tkn, nil + } + + return token.Token{}, token.NewPosError(tkn.Position, "not found visibility keyword.") +} + +func (p *Parser) ParseStateMutability() (ast.StateMutability, error) { + tkn, err := p.lexer.Scan() + if err != nil { + return token.Token{}, err + } + + switch tkn.Type { + case token.Pure, token.View, token.Payable: + return tkn, nil + } + + return token.Token{}, token.NewPosError(tkn.Position, "not found state-mutability keyword.") } -func (p *Parser) ParseModirierList() (*ast.ModifierList, error) { - return nil, nil +func (p *Parser) ParseFunctionDefinitionReturns() (*ast.FunctionDefinitionReturns, error) { + tkn, err := p.lexer.Peek() + if err != nil { + return nil, err + } + if tkn.Type != token.Returns { + return nil, nil + } + if _, err := p.lexer.Scan(); err != nil { + return nil, err + } + + lparen, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if lparen.Type != token.LParen { + return nil, token.NewPosError(lparen.Position, "not found arguments LParen.") + } + + pl, err := p.ParseParameterList() + if err != nil { + return nil, err + } + + rparen, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if rparen.Type != token.RParen { + return nil, token.NewPosError(rparen.Position, "not found arguments RParen.") + } + + return &ast.FunctionDefinitionReturns{ + From: tkn.Position, + LParen: lparen.Position, + ParameterList: pl, + RParen: rparen.Position, + }, nil } -func (p *Parser) ParseReturnParameters() (*ast.ReturnParameters, error) { - return nil, nil +func (p *Parser) ParseFunctionDefinition() (*ast.FunctionDefinition, error) { + from, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + if from.Type != token.Function { + return nil, token.NewPosError(from.Position, "not found function keyword.") + } + + dsc, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + switch dsc.Type { + case token.Identifier, token.From, token.Error, token.Revert, token.Global, token.Fallback, token.Receive: + default: + return nil, token.NewPosError(dsc.Position, "not found function description.") + } + + lparen, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if lparen.Type != token.LParen { + return nil, token.NewPosError(lparen.Position, "not found arguments LParen.") + } + + rparen, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if rparen.Type != token.RParen { + return nil, token.NewPosError(rparen.Position, "not found arguments RParen.") + } + + modifierList := &ast.ModifierList{ + Visibility: nil, + StateMutability: nil, + } + + for { + tkn, err := p.lexer.Peek() + if err != nil { + return nil, err + } + + switch tkn.Type { + case token.Internal, token.External, token.Public, token.Private: + vs, err := p.ParseVisibility() + if err != nil { + return nil, err + } + modifierList.Visibility = &vs + continue + case token.Pure, token.View, token.Payable: + sm, err := p.ParseStateMutability() + if err != nil { + return nil, err + } + modifierList.StateMutability = &sm + continue + } + break + } + + r, err := p.ParseFunctionDefinitionReturns() + if err != nil { + return nil, err + } + + b, err := p.ParseBlock() + if err != nil { + return nil, err + } + + return &ast.FunctionDefinition{ + From: from.Position, + FunctionDescriptor: dsc, + LParen: lparen.Position, + RParen: rparen.Position, + ModifierList: modifierList, + Returns: r, + Block: b, + }, nil } diff --git a/functionDefinition_test.go b/functionDefinition_test.go new file mode 100644 index 0000000..34aa549 --- /dev/null +++ b/functionDefinition_test.go @@ -0,0 +1,285 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseVisibility(t *testing.T) { + tests := []struct { + input string + want ast.Visibility + err *token.PosError + }{ + { + input: "internal", + want: ast.Visibility{ + Type: token.Internal, + Value: "internal", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + { + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{Column: 1, Line: 1}, + Msg: "not found visibility keyword.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseVisibility() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} + +func TestParser_ParseStateMutability(t *testing.T) { + tests := []struct { + input string + want ast.StateMutability + err *token.PosError + }{ + { + input: "pure", + want: ast.StateMutability{ + Type: token.Pure, + Value: "pure", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + { + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{Column: 1, Line: 1}, + Msg: "not found state-mutability keyword.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseStateMutability() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} + +func TestParser_ParseFunctionDefinitionReturns(t *testing.T) { + tests := []struct { + input string + want *ast.FunctionDefinitionReturns + err *token.PosError + }{ + { + input: "returns (string)", + want: &ast.FunctionDefinitionReturns{ + From: token.Pos{Column: 1, Line: 1}, + LParen: token.Pos{Column: 9, Line: 1}, + ParameterList: []*ast.Parameter{ + { + TypeName: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 10, Line: 1}, + }, + }, + }, + }, + RParen: token.Pos{Column: 16, Line: 1}, + }, + }, + { + input: ";", + want: nil, + }, + { + input: "returns string)", + err: &token.PosError{ + Pos: token.Pos{Column: 9, Line: 1}, + Msg: "not found arguments LParen.", + }, + }, + { + input: "returns (string {", + err: &token.PosError{ + Pos: token.Pos{Column: 17, Line: 1}, + Msg: "not found arguments RParen.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseFunctionDefinitionReturns() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} + +func TestParser_ParseFunctionDefinition(t *testing.T) { + tests := []struct { + name string + input string + want *ast.FunctionDefinition + err *token.PosError + }{ + { + name: "normal case", + input: `function hello() public pure returns (string) { + return "Hello World!!"; + }`, + want: &ast.FunctionDefinition{ + From: token.Pos{Column: 1, Line: 1}, + FunctionDescriptor: token.Token{ + Type: token.Identifier, + Value: "hello", + Position: token.Pos{Column: 10, Line: 1}, + }, + LParen: token.Pos{Column: 15, Line: 1}, + RParen: token.Pos{Column: 16, Line: 1}, + ModifierList: &ast.ModifierList{ + Visibility: &token.Token{ + Type: token.Public, + Value: "public", + Position: token.Pos{Column: 18, Line: 1}, + }, + StateMutability: &token.Token{ + Type: token.Pure, + Value: "pure", + Position: token.Pos{Column: 25, Line: 1}, + }, + }, + Returns: &ast.FunctionDefinitionReturns{ + From: token.Pos{Column: 30, Line: 1}, + LParen: token.Pos{Column: 38, Line: 1}, + ParameterList: []*ast.Parameter{ + { + TypeName: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 39, Line: 1}, + }, + }, + }, + }, + RParen: token.Pos{Column: 45, Line: 1}, + }, + Block: &ast.Block{ + LBracePos: token.Pos{Column: 47, Line: 1}, + RBracePos: token.Pos{Column: 5, Line: 3}, + Nodes: []ast.Node{ + &ast.ReturnStatement{ + From: token.Pos{Column: 9, Line: 2}, + SemiPos: token.Pos{Column: 31, Line: 2}, + Expression: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Position: token.Pos{Column: 16, Line: 2}, + Value: "\"Hello World!!\"", + }, + }, + }, + }, + }, + }, + { + name: "function description is invalid", + input: `function pragma() public pure returns (string) { + return "Hello World!!"; + }`, + err: &token.PosError{ + Pos: token.Pos{Column: 10, Line: 1}, + Msg: "not found function description.", + }, + }, + { + name: "not found lparen", + input: `function hello) public pure returns (string) { + return "Hello World!!"; + }`, + err: &token.PosError{ + Pos: token.Pos{Column: 15, Line: 1}, + Msg: "not found arguments LParen.", + }, + }, + { + name: "not found rparen", + input: `function hello( public pure returns (string) { + return "Hello World!!"; + }`, + err: &token.PosError{ + Pos: token.Pos{Column: 17, Line: 1}, + Msg: "not found arguments RParen.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseFunctionDefinition() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} diff --git a/go.mod b/go.mod index 5096641..c51c2e3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/uji/solparser -go 1.16 +go 1.18 require ( github.com/SteelSeries/bufrr v0.0.0-20161129220322-72103137aa3c diff --git a/helper_test.go b/helper_test.go new file mode 100644 index 0000000..43008db --- /dev/null +++ b/helper_test.go @@ -0,0 +1,74 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/token" +) + +func pos(c, l int) token.Pos { + return token.Pos{Column: c, Line: l} +} + +func posPtr(c, l int) *token.Pos { + return &token.Pos{Column: c, Line: l} +} + +func tkn(tp token.TokenType, text string, pos token.Pos) token.Token { + return token.Token{ + Type: tp, + Value: text, + Position: pos, + } +} + +func tknPtr(tp token.TokenType, text string, pos token.Pos) *token.Token { + return &token.Token{ + Type: tp, + Value: text, + Position: pos, + } +} + +func perr(pos token.Pos, msg string) *token.PosError { + return &token.PosError{ + Pos: pos, + Msg: msg, + } +} + +func assert(t *testing.T, got, want interface{}, gotErr, wantErr error) { + t.Helper() + var sErr *token.PosError + if errors.As(gotErr, &sErr) { + if diff := cmp.Diff(wantErr, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("%s", diff) + } +} + +type TestData[T any] []struct { + input string + want T + err error +} + +func (ts TestData[T]) Test(t *testing.T, target func(p *solparser.Parser) (T, error)) { + for _, tt := range ts { + tt := tt + t.Run(tt.input, func(t *testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + got, err := target(p) + assert(t, got, tt.want, err, tt.err) + }) + } +} diff --git a/identifier.go b/identifier.go new file mode 100644 index 0000000..37f42bb --- /dev/null +++ b/identifier.go @@ -0,0 +1,28 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func isIdentifier(tkn token.Token) bool { + switch tkn.Type { + case token.Identifier, token.From, token.Error, token.Revert, token.Global: + return true + } + return false +} + +func (p *Parser) ParseIdentifier() (ast.Identifier, error) { + tkn, err := p.lexer.Scan() + if err != nil { + return ast.Identifier{}, err + } + + switch tkn.Type { + case token.Identifier, token.From, token.Error, token.Revert, token.Global: + return ast.Identifier(tkn), nil + } + + return ast.Identifier{}, token.NewPosError(tkn.Position, "keyword is not available as identifier.") +} diff --git a/identifierPath.go b/identifierPath.go new file mode 100644 index 0000000..d46a506 --- /dev/null +++ b/identifierPath.go @@ -0,0 +1,41 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseIdentifierPath() (ast.IdentifierPath, error) { + id, err := p.ParseIdentifier() + if err != nil { + return ast.IdentifierPath{}, err + } + + var elements []*ast.IdentifierPathElement + for { + prd, err := p.lexer.Peek() + if err != nil || prd.Type != token.Period { + break + } + p.lexer.Scan() + + elements = append(elements, &ast.IdentifierPathElement{ + Identifier: ast.Identifier(id), + Period: &prd.Position, + }) + + i, err := p.ParseIdentifier() + if err != nil { + return ast.IdentifierPath{}, err + } + id = i + } + + elements = append(elements, &ast.IdentifierPathElement{ + Identifier: ast.Identifier(id), + }) + + return ast.IdentifierPath{ + Elements: elements, + }, nil +} diff --git a/identifierPath_test.go b/identifierPath_test.go new file mode 100644 index 0000000..eb9e631 --- /dev/null +++ b/identifierPath_test.go @@ -0,0 +1,32 @@ +package solparser_test + +import ( + "testing" + + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseIdentifierPath(t *testing.T) { + tests := TestData[ast.IdentifierPath]{ + { + input: "global . identifier", + want: ast.IdentifierPath{ + Elements: []*ast.IdentifierPathElement{ + { + Identifier: ast.Identifier(tkn(token.Global, "global", pos(1, 1))), + Period: posPtr(8, 1), + }, + { + Identifier: ast.Identifier(tkn(token.Identifier, "identifier", pos(10, 1))), + }, + }, + }, + }, + } + + tests.Test(t, func(p *solparser.Parser) (ast.IdentifierPath, error) { + return p.ParseIdentifierPath() + }) +} diff --git a/identifier_test.go b/identifier_test.go new file mode 100644 index 0000000..826c2ed --- /dev/null +++ b/identifier_test.go @@ -0,0 +1,80 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseIdentifier(t *testing.T) { + tests := []struct { + input string + want ast.Identifier + err *token.PosError + }{ + { + input: "identifier", + want: ast.Identifier{ + Type: token.Identifier, + Value: "identifier", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + { + input: "from", + want: ast.Identifier{ + Type: token.From, + Value: "from", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + { + input: "error", + want: ast.Identifier{ + Type: token.Error, + Value: "error", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + { + input: "global", + want: ast.Identifier{ + Type: token.Global, + Value: "global", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + { + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{Column: 1, Line: 1}, + Msg: "keyword is not available as identifier.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseIdentifier() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} diff --git a/importDirective.go b/importDirective.go new file mode 100644 index 0000000..2fff313 --- /dev/null +++ b/importDirective.go @@ -0,0 +1,169 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParsePath() (ast.Path, error) { + lit, err := p.lexer.Peek() + if err != nil { + return ast.Path{}, err + } + if lit.Type != token.NonEmptyStringLiteral { + return ast.Path{}, token.NewPosError(lit.Position, "not found non-empty-string-literal.") + } + if _, err := p.lexer.Scan(); err != nil { + return ast.Path{}, err + } + return ast.Path(lit), nil +} + +func (p *Parser) ParseImportDirectivePathElement() (*ast.ImportDirectivePathElement, error) { + path, err := p.ParsePath() + if err != nil { + return nil, err + } + + as, err := p.lexer.Peek() + if err != nil { + return nil, err + } + if as.Type != token.As { + return &ast.ImportDirectivePathElement{Path: path}, nil + } + if _, err := p.lexer.Scan(); err != nil { + return nil, err + } + + id, err := p.ParseIdentifier() + if err != nil { + return nil, err + } + + return &ast.ImportDirectivePathElement{ + Path: path, + As: &as.Position, + Identifier: &id, + }, nil +} + +func (p *Parser) ParseImportDirectiveSymbolAliasesElement() (*ast.ImportDirectiveSymbolAliasesElement, error) { + aliases, err := p.ParseSymbolAliases() + if err != nil { + return nil, err + } + + from, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if from.Type != token.From { + return nil, token.NewPosError(from.Position, "not found from keyword.") + } + + path, err := p.ParsePath() + if err != nil { + return nil, err + } + + return &ast.ImportDirectiveSymbolAliasesElement{ + SymbolAliases: aliases, + From: from.Position, + Path: path, + }, nil +} + +func (p *Parser) ParseImportDirectiveMulElement() (*ast.ImportDirectiveMulElement, error) { + mul, err := p.lexer.Peek() + if err != nil { + return nil, err + } + if mul.Type != token.Mul { + return nil, token.NewPosError(mul.Position, "not found mul keyword.") + } + if _, err := p.lexer.Scan(); err != nil { + return nil, err + } + + as, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if as.Type != token.As { + return nil, token.NewPosError(as.Position, "not found as keyword.") + } + + id, err := p.ParseIdentifier() + if err != nil { + return nil, err + } + + from, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if from.Type != token.From { + return nil, token.NewPosError(from.Position, "not found from keyword.") + } + + path, err := p.ParsePath() + if err != nil { + return nil, err + } + + return &ast.ImportDirectiveMulElement{ + Mul: mul.Position, + As: as.Position, + Identifier: id, + From: from.Position, + Path: path, + }, nil +} + +func (p *Parser) ParseImportDirective() (*ast.ImportDirective, error) { + impt, err := p.lexer.Peek() + if err != nil { + return nil, err + } + if impt.Type != token.Import { + return nil, token.NewPosError(impt.Position, "not found import keyword.") + } + if _, err := p.lexer.Scan(); err != nil { + return nil, err + } + + tkn, err := p.lexer.Peek() + if err != nil { + return nil, err + } + + var el ast.ImportDirectiveElement + switch tkn.Type { + case token.NonEmptyStringLiteral: + el, err = p.ParseImportDirectivePathElement() + case token.LBrace: + el, err = p.ParseImportDirectiveSymbolAliasesElement() + case token.Mul: + el, err = p.ParseImportDirectiveMulElement() + default: + return nil, token.NewPosError(tkn.Position, "not found import-directive element.") + } + if err != nil { + return nil, err + } + + semi, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if semi.Type != token.Semicolon { + return nil, token.NewPosError(semi.Position, "not found semicolon.") + } + + return &ast.ImportDirective{ + Import: impt.Position, + Element: el, + Semicolon: semi.Position, + }, nil +} diff --git a/importDirective_test.go b/importDirective_test.go new file mode 100644 index 0000000..aa91d4d --- /dev/null +++ b/importDirective_test.go @@ -0,0 +1,192 @@ +package solparser_test + +import ( + "testing" + + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParsePath(t *testing.T) { + tests := TestData[ast.Path]{ + { + input: `"test.sol"`, + want: ast.Path(tkn(token.NonEmptyStringLiteral, `"test.sol"`, pos(1, 1))), + }, + { + input: "pragma", + err: perr(pos(1, 1), "not found non-empty-string-literal."), + }, + } + + tests.Test(t, func(p *solparser.Parser) (ast.Path, error) { + return p.ParsePath() + }) +} + +func TestParser_ParseImportDirectivePathElement(t *testing.T) { + tests := TestData[*ast.ImportDirectivePathElement]{ + { + input: `"test.sol";`, + want: &ast.ImportDirectivePathElement{ + Path: ast.Path(tkn(token.NonEmptyStringLiteral, `"test.sol"`, pos(1, 1))), + }, + }, + { + input: `"test.sol" as alias1 ;`, + want: &ast.ImportDirectivePathElement{ + Path: ast.Path(tkn(token.NonEmptyStringLiteral, `"test.sol"`, pos(1, 1))), + As: posPtr(12, 1), + Identifier: &ast.Identifier{ + Type: token.Identifier, + Value: "alias1", + Position: pos(15, 1), + }, + }, + }, + } + + tests.Test(t, func(p *solparser.Parser) (*ast.ImportDirectivePathElement, error) { + return p.ParseImportDirectivePathElement() + }) +} + +func TestParser_ParseImportDirectiveSymbolAliasesElement(t *testing.T) { + tests := TestData[*ast.ImportDirectiveSymbolAliasesElement]{ + { + input: `{symbol1} from "test.sol"`, + want: &ast.ImportDirectiveSymbolAliasesElement{ + SymbolAliases: &ast.SymbolAliases{ + LBrace: pos(1, 1), + Aliases: []*ast.SymbolAlias{ + {Symbol: ast.Identifier{ + Type: token.Identifier, + Value: "symbol1", + Position: pos(2, 1), + }}, + }, + Commas: []*token.Pos{}, + RBrace: pos(9, 1), + }, + From: pos(11, 1), + Path: ast.Path(tkn(token.NonEmptyStringLiteral, `"test.sol"`, pos(16, 1))), + }, + }, + { + input: `{symbol1} "test.sol"`, + err: perr(pos(11, 1), "not found from keyword."), + }, + } + + tests.Test(t, func(p *solparser.Parser) (*ast.ImportDirectiveSymbolAliasesElement, error) { + return p.ParseImportDirectiveSymbolAliasesElement() + }) +} + +func TestParser_ParseImportDirectiveMulElement(t *testing.T) { + tests := TestData[*ast.ImportDirectiveMulElement]{ + { + input: `* as alias1 from "test.sol"`, + want: &ast.ImportDirectiveMulElement{ + Mul: pos(1, 1), + As: pos(3, 1), + Identifier: ast.Identifier{ + Type: token.Identifier, + Value: "alias1", + Position: pos(6, 1), + }, + From: pos(13, 1), + Path: ast.Path(tkn(token.NonEmptyStringLiteral, `"test.sol"`, pos(18, 1))), + }, + }, + { + input: `{symbol1} "test.sol"`, + err: perr(pos(1, 1), "not found mul keyword."), + }, + { + input: `* alias1 from "test.sol"`, + err: perr(pos(3, 1), "not found as keyword."), + }, + { + input: `* as alias1 "test.sol"`, + err: perr(pos(13, 1), "not found from keyword."), + }, + } + + tests.Test(t, func(p *solparser.Parser) (*ast.ImportDirectiveMulElement, error) { + return p.ParseImportDirectiveMulElement() + }) +} + +func TestParser_ParseImportDirective(t *testing.T) { + tests := TestData[*ast.ImportDirective]{ + { + input: `import "test.sol";`, + want: &ast.ImportDirective{ + Import: pos(1, 1), + Element: &ast.ImportDirectivePathElement{ + Path: ast.Path(tkn(token.NonEmptyStringLiteral, `"test.sol"`, pos(8, 1))), + }, + Semicolon: pos(18, 1), + }, + }, + { + input: `import {symbol1} from "test.sol";`, + want: &ast.ImportDirective{ + Import: pos(1, 1), + Element: &ast.ImportDirectiveSymbolAliasesElement{ + SymbolAliases: &ast.SymbolAliases{ + LBrace: pos(8, 1), + Aliases: []*ast.SymbolAlias{ + {Symbol: ast.Identifier{ + Type: token.Identifier, + Value: "symbol1", + Position: pos(9, 1), + }}, + }, + Commas: []*token.Pos{}, + RBrace: pos(16, 1), + }, + From: pos(18, 1), + Path: ast.Path(tkn(token.NonEmptyStringLiteral, `"test.sol"`, pos(23, 1))), + }, + Semicolon: pos(33, 1), + }, + }, + { + input: `import * as alias1 from "test.sol";`, + want: &ast.ImportDirective{ + Import: pos(1, 1), + Element: &ast.ImportDirectiveMulElement{ + Mul: pos(8, 1), + As: pos(10, 1), + Identifier: ast.Identifier{ + Type: token.Identifier, + Value: "alias1", + Position: pos(13, 1), + }, + From: pos(20, 1), + Path: ast.Path(tkn(token.NonEmptyStringLiteral, `"test.sol"`, pos(25, 1))), + }, + Semicolon: pos(35, 1), + }, + }, + { + input: "symbol as pragma", + err: perr(pos(1, 1), "not found import keyword."), + }, + { + input: `import alias1 from "test.sol";`, + err: perr(pos(8, 1), "not found import-directive element."), + }, + { + input: `import * as alias1 from "test.sol"`, + err: perr(pos(35, 1), "not found semicolon."), + }, + } + + tests.Test(t, func(p *solparser.Parser) (*ast.ImportDirective, error) { + return p.ParseImportDirective() + }) +} diff --git a/inheritanceSpecifier.go b/inheritanceSpecifier.go new file mode 100644 index 0000000..1c0cb24 --- /dev/null +++ b/inheritanceSpecifier.go @@ -0,0 +1,33 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseInheritanceSpecifire() (*ast.InheritanceSpecifier, error) { + ip, err := p.ParseIdentifierPath() + if err != nil { + return nil, err + } + + lparen, err := p.lexer.Peek() + if err != nil { + return nil, err + } + if lparen.Type != token.LParen { + return &ast.InheritanceSpecifier{ + IdentifierPath: ip, + }, nil + } + + cal, err := p.ParseCallArgumentList() + if err != nil { + return nil, err + } + + return &ast.InheritanceSpecifier{ + IdentifierPath: ip, + CallArgumentList: cal, + }, nil +} diff --git a/lexer/helper_test.go b/lexer/helper_test.go new file mode 100644 index 0000000..f02d7fd --- /dev/null +++ b/lexer/helper_test.go @@ -0,0 +1,56 @@ +package lexer + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser/token" +) + +func pos(c, l int) token.Pos { + return token.Pos{Column: c, Line: l} +} + +func tkn(tp token.TokenType, text string, pos token.Pos) token.Token { + return token.Token{ + Type: tp, + Value: text, + Position: pos, + } +} + +func perr(pos token.Pos, msg string) *token.PosError { + return &token.PosError{ + Pos: pos, + Msg: msg, + } +} + +type TestData[T any] []struct { + input string + want T + err error +} + +func (ts TestData[T]) Test(t *testing.T, target func(l *Lexer) (T, error)) { + for _, tt := range ts { + tt := tt + t.Run(tt.input, func(t *testing.T) { + l := New(strings.NewReader(tt.input)) + got, err := target(l) + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} diff --git a/lexer/lexer.go b/lexer/lexer.go index 970a8e6..ac7ef53 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -1,7 +1,9 @@ package lexer import ( + "errors" "io" + "unicode" "github.com/uji/solparser/scanner" "github.com/uji/solparser/token" @@ -10,10 +12,6 @@ import ( type Lexer struct { scanner *scanner.Scanner - // scan result - token token.Token - err error - // peek state peeked bool peekToken token.Token @@ -28,64 +26,181 @@ func New(input io.Reader) *Lexer { } } -func (l *Lexer) scan() (result bool, tkn token.Token, err error) { - pos, lit, err := l.scanner.Scan() +func (l *Lexer) scan() (tkn token.Token, err error) { + pos, str, err := l.scanner.Peek() if err != nil { - return false, token.Token{}, err + return token.Token{}, err + } + if str == "" { + return token.Token{}, errors.New("Empty character scanned.") + } + + if str == `"` || str == `\"` { + return l.ScanStringLiteral() + } + + l.scanner.Scan() + + // If space, scan for the next token + if token.IsSpace([]rune(str)[0]) { + return l.scan() } - return true, token.NewToken(lit, pos), nil + return token.NewToken(str, pos), nil } -func (l *Lexer) Scan() (result bool) { +func (l *Lexer) Scan() (token.Token, error) { if l.peeked { - l.token = l.peekToken - l.err = l.peekErr l.peeked = false - return true + return l.peekToken, l.peekErr } - rslt, tkn, err := l.scan() + return l.scan() +} + +func (l *Lexer) Peek() (token.Token, error) { + if l.peeked { + return l.peekToken, l.peekErr + } + + tkn, err := l.scan() + l.peeked = true + l.peekToken = tkn + l.peekErr = err + return l.peekToken, l.peekErr +} + +// ScanStringLiteral parse NonEmptyStringLiteral or EmptyStringLiteral then return StringLiteral token. +func (l *Lexer) ScanStringLiteral() (token.Token, error) { + start, v, err := l.scanner.Scan() if err != nil { - l.err = err - return false + return token.Token{}, err } - if !rslt { - return false + if v != `"` && v != `\'` { + return token.Token{}, token.NewPosError(start, `not found " or \'`) } - l.token = tkn - return true + quote, txt := v, v + tokenType := token.EmptyStringLiteral + for { + _, v, err := l.scanner.Scan() + if err != nil { + return token.Token{}, err + } + txt = txt + v + if v == quote { + return token.Token{ + Type: tokenType, + Value: txt, + Position: start, + }, nil + } + tokenType = token.NonEmptyStringLiteral + } } -func (l Lexer) Token() token.Token { - return l.token -} +const unicodeStringLiteralQuoteOffset = len("unicode") + +func (l *Lexer) ScanUnicodeStringLiteral() (token.Token, error) { + start, v, err := l.scanner.Scan() + if err != nil { + return token.Token{}, err + } + if v != "unicode" { + return token.Token{}, token.NewPosError(start, "not found unicode prefix.") + } + rslt := v + + pos, v, err := l.scanner.Scan() + if err != nil { + return token.Token{}, err + } + // 'unicode' and quote('"' or '\'') must not have spaces. + if v != `"` && v != `\'` && pos.Line == start.Line || pos.Column != start.Column+unicodeStringLiteralQuoteOffset { + return token.Token{}, token.NewPosError(pos, `not found " or \'`) + } -func (l Lexer) Error() error { - return l.err + rslt = rslt + v + quote := v + for { + _, v, err := l.scanner.Scan() + if err != nil { + return token.Token{}, err + } + rslt = rslt + v + if v == quote { + return token.Token{ + Type: token.UnicodeStringLiteral, + Value: rslt, + Position: start, + }, nil + } + } } -func (l *Lexer) Peek() bool { - if l.peeked { +func isEvenHexDigits(str string) bool { + if len(str) == 0 { return true } - - rslt, tkn, err := l.scan() - if !rslt { + runes := []rune(str) + if !unicode.Is(unicode.Hex_Digit, runes[0]) { return false } - l.peeked = true - l.peekToken = tkn - l.peekErr = err - return true + // If a chunk of HexString is being read, it will be true. + readingBlock := true + i := 1 + for i < len(runes) { + if !readingBlock && runes[i] == '_' { + i++ + continue + } + if !unicode.Is(unicode.Hex_Digit, runes[i]) { + return false + } + readingBlock = !readingBlock + i++ + } + return !readingBlock } -func (l Lexer) PeekToken() token.Token { - return l.peekToken -} +func (l *Lexer) ScanHexString() (token.Token, error) { + start, hex, err := l.scanner.Scan() + if err != nil { + return token.Token{}, err + } + + if hex != "hex" { + return token.Token{}, token.NewPosError(start, "not found hex prefix.") + } + + lqpos, lquote, err := l.scanner.Scan() + if err != nil { + return token.Token{}, err + } + if lquote != `"` && lquote != `\'` { + return token.Token{}, token.NewPosError(lqpos, `not found " or \'`) + } + + vpos, v, err := l.scanner.Scan() + if err != nil { + return token.Token{}, err + } + if !isEvenHexDigits(v) { + return token.Token{}, token.NewPosError(vpos, "invalid HexString format") + } + + rqpos, rquote, err := l.scanner.Scan() + if err != nil { + return token.Token{}, err + } + if rquote != lquote { + return token.Token{}, token.NewPosError(rqpos, `not found quote'`) + } -func (l Lexer) PeekError() error { - return l.peekErr + return token.Token{ + Type: token.HexString, + Value: hex + lquote + v + rquote, + Position: start, + }, nil } diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index 06a9e35..4551bc2 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -1,7 +1,7 @@ package lexer import ( - "io" + "errors" "strings" "testing" @@ -10,56 +10,45 @@ import ( "github.com/uji/solparser/token" ) -func TestLexerScan(t *testing.T) { +func TestLexer_Scan(t *testing.T) { tests := []struct { - name string - input string - peeked bool - peekToken token.Token - peekErr error - wantResult bool - wantErr error - wantToken token.Token + name string + input string + wantToken token.Token }{ { - name: "normal", - input: "pragma", - wantResult: true, + name: "normal", + input: "pragma", wantToken: token.Token{ - TokenType: token.Pragma, - Text: "pragma", - Pos: token.Pos{ + Type: token.Pragma, + Value: "pragma", + Position: token.Pos{ Column: 1, Line: 1, }, }, }, { - name: "when scan is done", - input: "", - wantResult: false, - wantToken: token.Token{}, - wantErr: io.EOF, - }, - { - name: "when peeked", - input: "", - peeked: true, - peekToken: token.Token{ - TokenType: token.BitXor, - Text: "^", - Pos: token.Pos{ - Column: 4, - Line: 6, + name: "There is a space at the beginning", + input: " pragma", + wantToken: token.Token{ + Type: token.Pragma, + Value: "pragma", + Position: token.Pos{ + Column: 2, + Line: 1, }, }, - wantResult: true, + }, + { + name: "There is a StringLiteral", + input: `"pragma"`, wantToken: token.Token{ - TokenType: token.BitXor, - Text: "^", - Pos: token.Pos{ - Column: 4, - Line: 6, + Type: token.NonEmptyStringLiteral, + Value: `"pragma"`, + Position: token.Pos{ + Column: 1, + Line: 1, }, }, }, @@ -68,52 +57,100 @@ func TestLexerScan(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { s := scanner.New(strings.NewReader(tt.input)) - l := Lexer{ - scanner: s, - peeked: tt.peeked, - peekToken: tt.peekToken, - peekErr: tt.peekErr, - } - if rslt := l.Scan(); rslt != tt.wantResult { - t.Errorf("result is wrong, want: %t, got: %t", tt.wantResult, rslt) + scanner: s, } - if err := l.Error(); err != tt.wantErr { - t.Errorf("error is wrong, want: %s, got: %s", tt.wantErr, err) + + tkn, err := l.Scan() + if err != nil { + t.Errorf("error is not nil, got: %s", err) } - if diff := cmp.Diff(tt.wantToken, l.Token()); diff != "" { + if diff := cmp.Diff(tt.wantToken, tkn); diff != "" { t.Errorf(diff) } }) } + + t.Run("when scan is done", func(t *testing.T) { + s := scanner.New(strings.NewReader("")) + l := Lexer{ + scanner: s, + } + + got, err := l.Scan() + if err != nil { + t.Errorf("got error: %s", err) + } + want := token.Token{ + Type: token.EOS, + Value: token.EOSString, + Position: token.Pos{ + Column: 1, + Line: 1, + }, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Error(diff) + } + }) + + t.Run("when peeked", func(t *testing.T) { + peekToken := token.Token{ + Type: token.BitXor, + Value: "^", + Position: token.Pos{ + Column: 4, + Line: 6, + }, + } + s := scanner.New(strings.NewReader("")) + l := Lexer{ + scanner: s, + peeked: true, + peekToken: peekToken, + peekErr: nil, + } + + tkn, err := l.Scan() + if err != nil { + t.Errorf("error is not nil, got: %s", err) + } + if diff := cmp.Diff(peekToken, tkn); diff != "" { + t.Errorf(diff) + } + }) } -func TestLexerPeek(t *testing.T) { +func TestLexer_Peek(t *testing.T) { tests := []struct { - name string - input string - result bool - token token.Token - err error + name string + input string + token token.Token + err error }{ { - name: "normal", - input: "pragma", - result: true, + name: "normal", + input: "pragma", token: token.Token{ - TokenType: token.Pragma, - Text: "pragma", - Pos: token.Pos{ + Type: token.Pragma, + Value: "pragma", + Position: token.Pos{ Column: 1, Line: 1, }, }, }, { - name: "when scan is done", - input: "", - result: false, - token: token.Token{}, + name: "when scan is done", + input: "", + token: token.Token{ + Type: token.EOS, + Value: token.EOSString, + Position: token.Pos{ + Column: 1, + Line: 1, + }, + }, }, } for _, tt := range tests { @@ -124,15 +161,165 @@ func TestLexerPeek(t *testing.T) { l := Lexer{ scanner: s, } - if rslt := l.Peek(); tt.result != rslt { - t.Errorf("result is wrong, want: %t, got: %t", tt.result, rslt) - } - if err := l.PeekError(); err != tt.err { + tkn, err := l.Peek() + if err != tt.err { t.Errorf("want: %s, got: %s", tt.err, err) } - if diff := cmp.Diff(tt.token, l.PeekToken()); diff != "" { + if diff := cmp.Diff(tt.token, tkn); diff != "" { + t.Errorf(diff) + } + }) + } +} + +func TestLexer_ScanStringLiteral(t *testing.T) { + tests := []struct { + input string + want token.Token + }{ + { + input: `"Hello world!!";`, + want: token.Token{ + Type: token.NonEmptyStringLiteral, + Value: `"Hello world!!"`, + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + { + input: `\'Hello world!!\';`, + want: token.Token{ + Type: token.NonEmptyStringLiteral, + Value: `\'Hello world!!\'`, + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + { + input: `"";`, + want: token.Token{ + Type: token.EmptyStringLiteral, + Value: `""`, + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + { + input: `\'\';`, + want: token.Token{ + Type: token.EmptyStringLiteral, + Value: `\'\'`, + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.input, func(t *testing.T) { + s := scanner.New(strings.NewReader(tt.input)) + l := Lexer{ + scanner: s, + } + + tkn, err := l.ScanStringLiteral() + if err != nil { + t.Errorf("error is not nil, got: %s", err) + } + if diff := cmp.Diff(tt.want, tkn); diff != "" { t.Errorf(diff) } }) } + + t.Run(`not found "`, func(t *testing.T) { + s := scanner.New(strings.NewReader("a")) + l := Lexer{ + scanner: s, + } + exptErr := &token.PosError{ + Pos: token.Pos{ + Column: 1, + Line: 1, + }, + Msg: `not found " or \'`, + } + + _, err := l.ScanStringLiteral() + var pErr *token.PosError + if !errors.As(err, &pErr) { + t.Errorf("error is unexpected, got: %s", err) + } + if diff := cmp.Diff(pErr, exptErr); diff != "" { + t.Errorf(diff) + } + }) +} + +func TestLexer_ScanUnicodeStringLiteral(t *testing.T) { + tests := TestData[token.Token]{ + { + input: `unicode"Hello 😃"`, + want: tkn(token.UnicodeStringLiteral, `unicode"Hello 😃"`, pos(1, 1)), + }, + { + input: `unicode\'Hello 😃\'`, + want: tkn(token.UnicodeStringLiteral, `unicode\'Hello 😃\'`, pos(1, 1)), + }, + { + input: `unicode Hello 😃`, + err: perr(pos(8, 1), `not found " or \'`), + }, + { + input: `unicode "Hello 😃"`, + err: perr(pos(8, 1), `not found " or \'`), + }, + { + input: `unicode + "Hello 😃"`, + err: perr(pos(8, 1), `not found " or \'`), + }, + } + + tests.Test(t, func(l *Lexer) (token.Token, error) { + return l.ScanUnicodeStringLiteral() + }) +} + +func TestLexer_ScanHexStringLiteral(t *testing.T) { + tests := TestData[token.Token]{ + { + input: `hex"1B"`, + want: tkn(token.HexString, `hex"1B"`, pos(1, 1)), + }, + { + input: `hex\'2E\'`, + want: tkn(token.HexString, `hex\'2E\'`, pos(1, 1)), + }, + { + input: `hex"16_3F_B5"`, + want: tkn(token.HexString, `hex"16_3F_B5"`, pos(1, 1)), + }, + { + input: `hex "12"`, + err: perr(pos(4, 1), `not found " or \'`), + }, + { + input: `hex + "123"`, + err: perr(pos(4, 1), `not found " or \'`), + }, + { + input: `hex" 123"`, + err: perr(pos(5, 1), "invalid HexString format"), + }, + { + input: `hex"16_3"`, + err: perr(pos(5, 1), "invalid HexString format"), + }, + { + input: `hex"163_A13"`, + err: perr(pos(5, 1), "invalid HexString format"), + }, + } + + tests.Test(t, func(l *Lexer) (token.Token, error) { + return l.ScanHexString() + }) } diff --git a/literal.go b/literal.go new file mode 100644 index 0000000..b2117d9 --- /dev/null +++ b/literal.go @@ -0,0 +1,20 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseLiteral() (ast.Literal, error) { + tkn, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + if tkn.Type != token.NonEmptyStringLiteral { + return nil, token.NewPosError(tkn.Position, "not found string literal quote") + } + + lit := ast.StringLiteral(tkn) + return &lit, nil +} diff --git a/literal_test.go b/literal_test.go new file mode 100644 index 0000000..0082803 --- /dev/null +++ b/literal_test.go @@ -0,0 +1,87 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseLiteral(t *testing.T) { + tests := []struct { + name string + input string + want ast.Literal + err *token.PosError + }{ + { + name: "normal", + input: `"Hello World!!";`, + want: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Value: `"Hello World!!"`, + Position: pos(1, 1), + }, + }, + { + name: `Including \n`, + input: "\"Hello \nWorld!!\";", + want: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Value: "\"Hello \nWorld!!\"", + Position: pos(1, 1), + }, + }, + // { + // name: `Next token is \n`, + // input: "\"Hello World!!\"\n", + // want: &ast.StringLiteral{ + // Value: `"Hello World!!"`, + // From: token.Pos{ + // Column: 1, + // Line: 1, + // }, + // To: token.Pos{ + // Column: 15, + // Line: 1, + // }, + // }, + // err: nil, + // }, + { + name: "Not Literal", + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{ + Column: 1, + Line: 1, + }, + Msg: "not found string literal quote", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseLiteral() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} diff --git a/parameterList.go b/parameterList.go new file mode 100644 index 0000000..b97bd8d --- /dev/null +++ b/parameterList.go @@ -0,0 +1,40 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseParameter() (*ast.Parameter, error) { + tn, err := p.ParseTypeName() + if err != nil { + return nil, err + } + + return &ast.Parameter{ + TypeName: tn, + }, nil +} + +func (p *Parser) ParseParameterList() (ast.ParameterList, error) { + prms := make(ast.ParameterList, 0, 1) + for { + prm, err := p.ParseParameter() + if err != nil { + return nil, err + } + + prms = append(prms, prm) + + comma, err := p.lexer.Peek() + if err != nil { + return nil, err + } + + if comma.Type != token.Comma { + return prms, nil + } + + p.lexer.Scan() + } +} diff --git a/parameterList_test.go b/parameterList_test.go new file mode 100644 index 0000000..809bc45 --- /dev/null +++ b/parameterList_test.go @@ -0,0 +1,149 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseParameter(t *testing.T) { + tests := []struct { + name string + input string + want *ast.Parameter + err *token.PosError + }{ + { + name: "TypeName only", + input: "string", + want: &ast.Parameter{ + TypeName: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + }, + err: nil, + }, + { + name: "Not ParameterList", + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{ + Column: 1, + Line: 1, + }, + Msg: "not found type-name.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseParameter() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} + +func TestParser_ParseParameterList(t *testing.T) { + tests := []struct { + name string + input string + want ast.ParameterList + err *token.PosError + }{ + { + name: "One Parameter", + input: "string", + want: ast.ParameterList{ + { + TypeName: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + }, + }, + err: nil, + }, + { + name: "Some Parameter", + input: "string, bool", + want: ast.ParameterList{ + { + TypeName: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + }, + { + TypeName: ast.ElementaryTypeName{ + { + Type: token.Bool, + Value: "bool", + Position: token.Pos{Column: 9, Line: 1}, + }, + }, + }, + }, + err: nil, + }, + { + name: "Not ParameterList", + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{ + Column: 1, + Line: 1, + }, + Msg: "not found type-name.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseParameterList() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} diff --git a/pragmaDirective.go b/pragmaDirective.go index 2a9b689..43cda5f 100644 --- a/pragmaDirective.go +++ b/pragmaDirective.go @@ -6,39 +6,33 @@ import ( ) func (p *Parser) ParsePragmaDirective() (*ast.PragmaDirective, error) { - p.lexer.Scan() - if tkn := p.lexer.Token(); tkn.TokenType != token.Pragma { - return nil, newError(tkn.Pos, "not found pragma") + prgm, err := p.lexer.Scan() + if err != nil { + return nil, err } - - p.lexer.Scan() - pragmaName := p.lexer.Token() - if pragmaName.TokenType != token.Unknown { - return nil, newError(pragmaName.Pos, "not found pragma name") - } - - p.lexer.Scan() - expression := p.lexer.Token() - if expression.TokenType != token.BitXor { - return nil, newError(expression.Pos, "not found BitXor expression") + if prgm.Type != token.Pragma { + return nil, token.NewPosError(prgm.Position, "not found pragma.") } - p.lexer.Scan() - version := p.lexer.Token() - if version.TokenType != token.Unknown { - return nil, newError(version.Pos, "not found version") + tkns := make([]*token.Token, 0, 1) + for { + tkn, err := p.lexer.Scan() + if err != nil { + return nil, err + } + if tkn.Type == token.EOS { + return nil, token.NewPosError(tkn.Position, "not found Semicolon.") + } + if tkn.Type == token.Semicolon { + if len(tkns) == 0 { + return nil, token.NewPosError(tkn.Position, "not found pragma tokens.") + } + return &ast.PragmaDirective{ + Pragma: prgm.Position, + PragmaTokens: tkns, + Semicolon: tkn.Position, + }, nil + } + tkns = append(tkns, &tkn) } - - p.lexer.Scan() - if tkn := p.lexer.Token(); tkn.TokenType != token.Semicolon { - return nil, newError(tkn.Pos, "not found Semicolon") - } - - return &ast.PragmaDirective{ - PragmaName: pragmaName.Text, - PragmaValue: ast.PragmaValue{ - Version: version.Text, - Expression: expression.Text, - }, - }, nil } diff --git a/pragmaDirective_test.go b/pragmaDirective_test.go index 08b38b0..192f270 100644 --- a/pragmaDirective_test.go +++ b/pragmaDirective_test.go @@ -1,102 +1,44 @@ package solparser_test import ( - "errors" "strings" "testing" - "github.com/google/go-cmp/cmp" "github.com/uji/solparser" "github.com/uji/solparser/ast" "github.com/uji/solparser/token" ) -func TestParserParsePragmaDirective(t *testing.T) { +func TestParser_ParsePragmaDirective(t *testing.T) { tests := []struct { - name string input string want *ast.PragmaDirective - err *solparser.Error + err *token.PosError }{ { - name: "normal case", input: "pragma solidity ^0.8.13;", want: &ast.PragmaDirective{ - PragmaName: "solidity", - PragmaValue: ast.PragmaValue{ - Version: "0.8.13", - Expression: "^", + Pragma: pos(1, 1), + PragmaTokens: []*token.Token{ + {Type: token.Identifier, Value: "solidity", Position: pos(8, 1)}, + {Type: token.BitXor, Value: "^", Position: pos(17, 1)}, + {Type: token.Identifier, Value: "0.8.13", Position: pos(18, 1)}, }, - }, - err: nil, - }, - { - name: "there is no pragma keyword", - input: "solidity ^0.8.13;", - want: nil, - err: &solparser.Error{ - Pos: token.Pos{ - Column: 1, - Line: 1, - }, - Msg: "not found pragma", - }, - }, - { - name: "there is no pragma name", - input: "pragma ^0.8.13;", - want: nil, - err: &solparser.Error{ - Pos: token.Pos{ - Column: 8, - Line: 1, - }, - Msg: "not found pragma name", - }, - }, - { - name: "there is no BitXor expression", - input: "pragma solidity 0.8.13;", - want: nil, - err: &solparser.Error{ - Pos: token.Pos{ - Column: 17, - Line: 1, - }, - Msg: "not found BitXor expression", - }, - }, - { - name: "there is no Semicolon", - input: "pragma solidity ^0.8.13", - want: nil, - err: &solparser.Error{ - Pos: token.Pos{ - Column: 18, - Line: 1, - }, - Msg: "not found Semicolon", + Semicolon: pos(24, 1), }, }, + {input: "solidity ^0.8.13;", err: perr(pos(1, 1), "not found pragma.")}, + {input: "pragma ;", err: perr(pos(8, 1), "not found pragma tokens.")}, + {input: "pragma solidity ^0.8.13", err: perr(pos(24, 1), "not found Semicolon.")}, } for _, tt := range tests { - t.Run(tt.name, func(*testing.T) { + t.Run(tt.input, func(*testing.T) { r := strings.NewReader(tt.input) p := solparser.New(r) got, err := p.ParsePragmaDirective() - - var sErr *solparser.Error - if errors.As(err, &sErr) { - if diff := cmp.Diff(tt.err, sErr); diff != "" { - t.Errorf("%s", diff) - } - } - - if diff := cmp.Diff(tt.want, got); diff != "" { - t.Errorf("%s", diff) - } + assert(t, got, tt.want, err, tt.err) }) } } diff --git a/scanner/scanner.go b/scanner/scanner.go index 589e414..ee68301 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -1,6 +1,7 @@ package scanner import ( + "errors" "io" "github.com/SteelSeries/bufrr" @@ -14,6 +15,12 @@ type Scanner struct { // position state offset int lineOffset int + + // peek state + peeked bool + peekStr string + peekPos token.Pos + peekErr error } func New(reader io.Reader) *Scanner { @@ -23,40 +30,7 @@ func New(reader io.Reader) *Scanner { } } -// isSpace reports whether the character is a Unicode white space character. -// We avoid dependency on the unicode package, but check validity of the implementation -// in the tests. -func isSpace(r rune) bool { - if r <= '\u00FF' { - // Obvious ASCII ones: \t through \r plus space. Plus two Latin-1 oddballs. - switch r { - case ' ', '\t', '\v', '\f', '\r': - return true - case '\u0085', '\u00A0': - return true - } - return false - } - // High-valued ones. - if '\u2000' <= r && r <= '\u200a' { - return true - } - switch r { - case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000': - return true - } - return false -} - -func isMultiLengthOperatorSymbol(r rune) bool { - switch r { - case '=', '|', '^', '&', '+', '-', '*', '/', '%', '<', '>', '!': - return true - } - return false -} - -func isSplitSymbol(r rune) bool { +func isOperatorRune(r rune) bool { switch r { case '(', ')', '[', ']', '{', '}', ':', ';', '?', '=', '|', '^', '&', '<', '>', '+', '-', '*', '/', '%', ',', '!', '~', '"', '\'', '\\': return true @@ -80,53 +54,154 @@ func (s *Scanner) readRune() (rune, error) { return r, nil } -func (s *Scanner) Scan() (token.Pos, string, error) { - runes := make([]rune, 1) - for { - // Retain the state before reading. - c := s.offset + 1 - l := s.lineOffset + 1 +var ( + errNotOperator = errors.New("Not operator.") +) - ch, err := s.readRune() +func (s *Scanner) scanOperator() (string, error) { + ch1, _, err := s.r.PeekRune() + if err != nil { + return "", err + } + if !isOperatorRune(ch1) { + return "", errNotOperator + } + if _, err := s.readRune(); err != nil { + return "", err + } + ch2, _, err := s.r.PeekRune() + if err != nil { + return string(ch1), nil + } + oprt := string([]rune{ch1, ch2}) + switch oprt { + case "=>", "->", "|=", "^=", "&=", "+=", "-=", "*=", "/=", "%=", "==", "||", "&&", "**", "!=", "<-", ">-", "++", "--", `\'`: + if _, err := s.readRune(); err != nil { + return "", err + } + return oprt, nil + case "<<": + if _, err := s.readRune(); err != nil { + return "", err + } + ch3, _, err := s.r.PeekRune() if err != nil { - return token.Pos{}, "", err + return oprt, nil } - if ch == bufrr.EOF { - return token.Pos{}, "", io.EOF + if ch3 == '=' { + if _, err := s.readRune(); err != nil { + return "", err + } + return "<<=", nil } - if isSplitSymbol(ch) || ch == '\n' { - return token.Pos{ - Column: c, - Line: l, - }, string(ch), nil + return oprt, nil + case ">>": + if _, err := s.readRune(); err != nil { + return "", err + } + ch3, _, err := s.r.PeekRune() + if err != nil { + return oprt, nil } - if !isSpace(ch) { - runes[0] = ch - break + if ch3 == '=' { + if _, err := s.readRune(); err != nil { + return "", err + } + return ">>=", nil } + if ch3 == '>' { + if _, err := s.readRune(); err != nil { + return "", err + } + oprt = ">>>" + ch4, _, err := s.r.PeekRune() + if err != nil { + return oprt, nil + } + if ch4 == '=' { + if _, err := s.readRune(); err != nil { + return "", err + } + return ">>>=", nil + } + return oprt, nil + } + return oprt, nil } + return string(ch1), nil +} - pos := token.Pos{ - Column: s.offset, +// scan flow +// +// - If first rune is operator character, scan operator. +// - If first rune is a space character, scan until the end of the blank character. +// - Else scan to next space or operator string. +func (s *Scanner) scan() (token.Pos, string, error) { + startPos := token.Pos{ + Column: s.offset + 1, Line: s.lineOffset + 1, } + ch, _, err := s.r.PeekRune() + if err != nil { + return token.Pos{}, "", err + } + if ch == bufrr.EOF { + return startPos, token.EOSString, nil + } + if isOperatorRune(ch) { + // scan operator. + oprt, err := s.scanOperator() + if err != nil { + return token.Pos{}, "", err + } + return startPos, oprt, nil + } + + if _, err := s.readRune(); err != nil { + return token.Pos{}, "", err + } + + readingSpace := token.IsSpace(ch) + rslt := []rune{ch} + for { ch, _, err := s.r.PeekRune() if err != nil { return token.Pos{}, "", err } - if isSpace(ch) || isSplitSymbol(ch) || ch == '\n' || ch == bufrr.EOF { - return pos, string(runes), nil + if token.IsSpace(ch) != readingSpace || isOperatorRune(ch) || ch == bufrr.EOF { + return startPos, string(rslt), nil } - ch, err = s.readRune() - if err != nil { + if _, err = s.readRune(); err != nil { return token.Pos{}, "", err } - runes = append(runes, ch) + rslt = append(rslt, ch) } } -func (s *Scanner) Peek() (pos token.Pos, lit string, err error) { - return token.Pos{}, "", nil +// Scan divides a string into the smallest units from the bufrr.Reader and returns them one by one. +func (s *Scanner) Scan() (token.Pos, string, error) { + if s.peeked { + s.peeked = false + return s.peekPos, s.peekStr, s.peekErr + } + + return s.scan() +} + +// Peek reads ahead and returns the result of Scan. +// The offset is unchanged. +func (s *Scanner) Peek() (pos token.Pos, str string, err error) { + if s.peeked { + return s.peekPos, s.peekStr, s.peekErr + } + + s.peeked = true + + pos, str, err = s.scan() + s.peekPos = pos + s.peekStr = str + s.peekErr = err + return } diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index c270afc..85bedb2 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -1,77 +1,80 @@ package scanner import ( - "io" + "errors" "strings" "testing" + "github.com/SteelSeries/bufrr" "github.com/google/go-cmp/cmp" "github.com/uji/solparser/token" ) -func TestScannerScan(t *testing.T) { +func TestScanner_Scan(t *testing.T) { tests := []struct { name string input string wantPoss []token.Pos - wantLits []string + wantStrs []string }{ { name: "normal", input: "pragma solidity ^0.8.13;", wantPoss: []token.Pos{ - { - Column: 1, - Line: 1, - }, - { - Column: 8, - Line: 1, - }, - { - Column: 17, - Line: 1, - }, - { - Column: 18, - Line: 1, - }, - { - Column: 24, - Line: 1, - }, + {Column: 1, Line: 1}, + {Column: 7, Line: 1}, + {Column: 8, Line: 1}, + {Column: 16, Line: 1}, + {Column: 17, Line: 1}, + {Column: 18, Line: 1}, + {Column: 24, Line: 1}, }, - wantLits: []string{ + wantStrs: []string{ "pragma", + " ", "solidity", + " ", "^", "0.8.13", ";", }, }, { - name: "normal", + name: "there is newline character", input: "pragma\n solidity", wantPoss: []token.Pos{ - { - Column: 1, - Line: 1, - }, - { - Column: 7, - Line: 1, - }, - { - Column: 2, - Line: 2, - }, + {Column: 1, Line: 1}, + {Column: 7, Line: 1}, + {Column: 2, Line: 2}, }, - wantLits: []string{ + wantStrs: []string{ "pragma", - "\n", + "\n ", "solidity", }, }, + { + name: "there are operators", + input: `a >> \'test\'`, + wantPoss: []token.Pos{ + {Column: 1, Line: 1}, + {Column: 2, Line: 1}, + {Column: 3, Line: 1}, + {Column: 5, Line: 1}, + {Column: 6, Line: 1}, + {Column: 8, Line: 1}, + {Column: 12, Line: 1}, + }, + wantStrs: []string{ + "a", + " ", + ">>", + " ", + `\'`, + "test", + `\'`, + }, + }, } for _, tt := range tests { @@ -80,26 +83,158 @@ func TestScannerScan(t *testing.T) { s := New(strings.NewReader(tt.input)) poss := make([]token.Pos, 0, len(tt.wantPoss)) - lits := make([]string, 0, len(tt.wantLits)) + strs := make([]string, 0, len(tt.wantStrs)) for { - pos, lit, err := s.Scan() - if err == io.EOF { - break - } + pos, str, err := s.Scan() if err != nil { t.Fatalf("got unexpected error: %s", err) } + if str == token.EOSString { + break + } poss = append(poss, pos) - lits = append(lits, lit) + strs = append(strs, str) } if diff := cmp.Diff(tt.wantPoss, poss); diff != "" { t.Errorf(diff) } - if diff := cmp.Diff(tt.wantLits, lits); diff != "" { + if diff := cmp.Diff(tt.wantStrs, strs); diff != "" { t.Errorf(diff) } }) } + + t.Run("when peeked", func(t *testing.T) { + wantStr := "str" + wantPos := token.Pos{ + Column: 3, + Line: 4, + } + wantErr := errors.New(t.Name()) + s := &Scanner{ + r: bufrr.NewReader(strings.NewReader("input will not scand")), + peeked: true, + peekStr: wantStr, + peekPos: wantPos, + peekErr: wantErr, + } + + pos, str, err := s.Scan() + if !errors.Is(err, wantErr) { + t.Fatalf("got unexpected error: %s", err) + } + if diff := cmp.Diff(wantPos, pos); diff != "" { + t.Errorf(diff) + } + if str != wantStr { + t.Errorf("got %s, want %s", str, wantStr) + } + if s.peeked != false { + t.Error("peeked status was not cleared") + } + }) +} + +func TestScanner_Peek(t *testing.T) { + terr := errors.New(t.Name()) + tests := []struct { + name string + input string + peeked bool + peekedPos token.Pos + peekedStr string + peekedErr error + wantPos token.Pos + wantStr string + wantErr error + }{ + { + name: "not peeked", + input: "pragma solidity ^0.8.13;", + peeked: false, + wantPos: token.Pos{ + Column: 1, + Line: 1, + }, + wantStr: "pragma", + }, + { + name: "peeked", + peeked: true, + peekedStr: "peekedStr", + peekedPos: token.Pos{ + Column: 2, + Line: 2, + }, + wantPos: token.Pos{ + Column: 2, + Line: 2, + }, + wantErr: terr, + wantStr: "peekedStr", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + s := &Scanner{ + r: bufrr.NewReader(strings.NewReader(tt.input)), + peeked: tt.peeked, + peekStr: tt.peekedStr, + peekPos: tt.peekedPos, + peekErr: tt.peekedErr, + } + + pos, str, err := s.Peek() + if !errors.Is(err, tt.peekedErr) { + t.Fatalf("got unexpected error: %s", err) + } + if diff := cmp.Diff(tt.wantPos, pos); diff != "" { + t.Errorf(diff) + } + if str != tt.wantStr { + t.Errorf("got %s, want %s", str, tt.wantStr) + } + }) + } +} + +func TestScanner_scanOperator(t *testing.T) { + tests := []struct { + input string + want string + wantErr error + }{ + {input: "^0.8.13", want: "^"}, + {input: "=>>", want: "=>"}, + {input: "<< ", want: "<<"}, + {input: "<<=a", want: "<<="}, + {input: ">>1", want: ">>"}, + {input: ">>=x", want: ">>="}, + {input: ">>>>", want: ">>>"}, + {input: ">>>==", want: ">>>="}, + {input: "pragma", wantErr: errNotOperator}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.input, func(t *testing.T) { + s := New(strings.NewReader(tt.input)) + + got, err := s.scanOperator() + if !errors.Is(err, tt.wantErr) { + t.Error(err) + } + if got != tt.want { + t.Errorf("got: %s, want: %s", got, tt.want) + } + expectedOffset := len(tt.want) + if s.offset != expectedOffset { + t.Errorf("offset is %d, expected is %d", s.offset, expectedOffset) + } + }) + } } diff --git a/solparser.go b/solparser.go index 680742b..a2a02fd 100644 --- a/solparser.go +++ b/solparser.go @@ -8,6 +8,7 @@ import ( "github.com/uji/solparser/token" ) +// Parser parses "Solidity" code and outputs ASTs. type Parser struct { input io.Reader lexer *lexer.Lexer @@ -20,62 +21,66 @@ func New(input io.Reader) *Parser { } } -func (p *Parser) Parse(input io.Reader) (*ast.SourceUnit, error) { - if !p.lexer.Peek() { - return nil, nil - } - - if err := p.lexer.PeekError(); err != nil { - return nil, err - } - +func (p *Parser) Parse() (*ast.SourceUnit, error) { var pragmaDirective *ast.PragmaDirective + var importDirective *ast.ImportDirective var contractDefinition *ast.ContractDefinition var functionDefinition *ast.FunctionDefinition - - switch p.lexer.PeekToken().TokenType { - case token.Pragma: - prgm, err := p.ParsePragmaDirective() - if err != nil { - return nil, err - } - pragmaDirective = prgm - case token.Abstract, token.Contract: - cntrct, err := p.ParseContractDefinition() + for { + tkn, err := p.lexer.Peek() if err != nil { return nil, err } - contractDefinition = cntrct - case token.Function: - fnc, err := p.ParseFunctionDefinition() - if err != nil { - return nil, err + + switch tkn.Type { + case token.Pragma: + prgm, err := p.ParsePragmaDirective() + if err != nil { + return nil, err + } + pragmaDirective = prgm + case token.Import: + imp, err := p.ParseImportDirective() + if err != nil { + return nil, err + } + importDirective = imp + case token.Abstract, token.Contract: + cntrct, err := p.ParseContractDefinition() + if err != nil { + return nil, err + } + contractDefinition = cntrct + case token.Function: + fnc, err := p.ParseFunctionDefinition() + if err != nil { + return nil, err + } + functionDefinition = fnc + case token.EOS: + return &ast.SourceUnit{ + PragmaDirective: pragmaDirective, + ImportDirective: importDirective, + ContractDefinition: contractDefinition, + FunctionDefinition: functionDefinition, + }, nil + default: + return nil, token.NewPosError(tkn.Position, "invalid") } - functionDefinition = fnc } - - return &ast.SourceUnit{ - PragmaDirective: pragmaDirective, - ContractDefinition: contractDefinition, - FunctionDefinition: functionDefinition, - }, nil } func (p *Parser) ParseBooleanLiteral() (*ast.BooleanLiteral, error) { - if !p.lexer.Scan() { - return nil, nil - } - - if err := p.lexer.Error(); err != nil { + tkn, err := p.lexer.Scan() + if err != nil { return nil, err } - tkn := p.lexer.Token() - if tkn.TokenType != token.True && tkn.TokenType != token.False { - return nil, newError(tkn.Pos, "not found keyword true or false") + if tkn.Type != token.TrueLiteral && tkn.Type != token.FalseLiteral { + return nil, token.NewPosError(tkn.Position, "not found keyword true or false") } return &ast.BooleanLiteral{ - Token: p.lexer.Token(), + Token: tkn, }, nil } diff --git a/solparser_test.go b/solparser_test.go index ac14890..b2f12ea 100644 --- a/solparser_test.go +++ b/solparser_test.go @@ -11,21 +11,149 @@ import ( "github.com/uji/solparser/token" ) -func TestParserParseBooleanLiteral(t *testing.T) { +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + input string + want *ast.SourceUnit + err *token.PosError + }{ + { + name: "normal", + input: `pragma solidity ^0.8.13; + +contract HelloWorld { + function hello() public pure returns (string) { + return "Hello World!!"; + } +}`, + want: &ast.SourceUnit{ + PragmaDirective: &ast.PragmaDirective{ + Pragma: token.Pos{Column: 1, Line: 1}, + PragmaTokens: []*token.Token{ + { + Type: token.Identifier, + Value: "solidity", + Position: token.Pos{Column: 8, Line: 1}, + }, + { + Type: token.BitXor, + Value: "^", + Position: token.Pos{Column: 17, Line: 1}, + }, + { + Type: token.Identifier, + Value: "0.8.13", + Position: token.Pos{Column: 18, Line: 1}, + }, + }, + Semicolon: token.Pos{Column: 24, Line: 1}, + }, + ContractDefinition: &ast.ContractDefinition{ + Contract: token.Pos{Column: 1, Line: 3}, + Identifier: ast.Identifier{ + Type: token.Identifier, + Value: "HelloWorld", + Position: token.Pos{Column: 10, Line: 3}, + }, + LBrace: token.Pos{Column: 21, Line: 3}, + ContractBodyElements: []ast.ContractBodyElement{ + &ast.FunctionDefinition{ + From: token.Pos{Column: 5, Line: 4}, + FunctionDescriptor: token.Token{ + Type: token.Identifier, + Value: "hello", + Position: token.Pos{Column: 14, Line: 4}, + }, + LParen: token.Pos{Column: 19, Line: 4}, + RParen: token.Pos{Column: 20, Line: 4}, + ModifierList: &ast.ModifierList{ + Visibility: &token.Token{ + Type: token.Public, + Value: "public", + Position: token.Pos{Column: 22, Line: 4}, + }, + StateMutability: &token.Token{ + Type: token.Pure, + Value: "pure", + Position: token.Pos{Column: 29, Line: 4}, + }, + }, + Returns: &ast.FunctionDefinitionReturns{ + From: token.Pos{Column: 34, Line: 4}, + LParen: token.Pos{Column: 42, Line: 4}, + ParameterList: []*ast.Parameter{ + { + TypeName: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 43, Line: 4}, + }, + }, + }, + }, + RParen: token.Pos{Column: 49, Line: 4}, + }, + Block: &ast.Block{ + LBracePos: token.Pos{Column: 51, Line: 4}, + RBracePos: token.Pos{Column: 5, Line: 6}, + Nodes: []ast.Node{ + &ast.ReturnStatement{ + From: token.Pos{Column: 9, Line: 5}, + SemiPos: token.Pos{Column: 31, Line: 5}, + Expression: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Position: token.Pos{Column: 16, Line: 5}, + Value: "\"Hello World!!\"", + }, + }, + }, + }, + }, + }, + RBrace: token.Pos{Column: 1, Line: 7}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.Parse() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} + +func TestParser_ParseBooleanLiteral(t *testing.T) { tests := []struct { name string input string want *ast.BooleanLiteral - err *solparser.Error + err *token.PosError }{ { name: "true case", input: "true", want: &ast.BooleanLiteral{ Token: token.Token{ - TokenType: token.True, - Text: "true", - Pos: token.Pos{ + Type: token.TrueLiteral, + Value: "true", + Position: token.Pos{ Column: 1, Line: 1, }, @@ -38,9 +166,9 @@ func TestParserParseBooleanLiteral(t *testing.T) { input: "false", want: &ast.BooleanLiteral{ Token: token.Token{ - TokenType: token.False, - Text: "false", - Pos: token.Pos{ + Type: token.FalseLiteral, + Value: "false", + Position: token.Pos{ Column: 1, Line: 1, }, @@ -52,7 +180,7 @@ func TestParserParseBooleanLiteral(t *testing.T) { name: "not true or false", input: "solidity", want: nil, - err: &solparser.Error{ + err: &token.PosError{ Pos: token.Pos{ Column: 1, Line: 1, @@ -69,7 +197,7 @@ func TestParserParseBooleanLiteral(t *testing.T) { got, err := p.ParseBooleanLiteral() - var sErr *solparser.Error + var sErr *token.PosError if errors.As(err, &sErr) { if diff := cmp.Diff(tt.err, sErr); diff != "" { t.Errorf("%s", diff) diff --git a/statement.go b/statement.go new file mode 100644 index 0000000..0184548 --- /dev/null +++ b/statement.go @@ -0,0 +1,50 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseStatement() (ast.Statement, error) { + tkn, err := p.lexer.Peek() + if err != nil { + return nil, err + } + + if tkn.Type == token.Return { + return p.ParseReturnStatement() + } + + return nil, token.NewPosError(tkn.Position, "not found statement.") +} + +func (p *Parser) ParseReturnStatement() (ast.Statement, error) { + rtn, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + if rtn.Type != token.Return { + return nil, token.NewPosError(rtn.Position, "not found return keyword.") + } + + exp, err := p.ParseExpression() + if err != nil { + return nil, err + } + + semi, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + if semi.Type != token.Semicolon { + return nil, token.NewPosError(semi.Position, "not found semicolon.") + } + + return &ast.ReturnStatement{ + From: rtn.Position, + SemiPos: semi.Position, + Expression: exp, + }, nil +} diff --git a/statement_test.go b/statement_test.go new file mode 100644 index 0000000..ff0cb66 --- /dev/null +++ b/statement_test.go @@ -0,0 +1,140 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseStatement(t *testing.T) { + tests := []struct { + name string + input string + want ast.Statement + err *token.PosError + }{ + { + name: "ReturnStatement", + input: `return "Hello World!!";`, + want: &ast.ReturnStatement{ + From: token.Pos{Column: 1, Line: 1}, + SemiPos: token.Pos{Column: 23, Line: 1}, + Expression: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Position: token.Pos{Column: 8, Line: 1}, + Value: `"Hello World!!"`, + }, + }, + err: nil, + }, + { + name: "Not Statement", + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{Column: 1, Line: 1}, + Msg: "not found statement.", + }, + }, + { + name: "Found broken return statement", + input: "return pragma", + err: &token.PosError{ + Pos: token.Pos{Column: 8, Line: 1}, + Msg: "not found expression.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseStatement() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} + +func TestParser_ParseReturnStatement(t *testing.T) { + tests := []struct { + name string + input string + want ast.Statement + err *token.PosError + }{ + { + name: "normal", + input: `return "Hello World!!";`, + want: &ast.ReturnStatement{ + From: token.Pos{Column: 1, Line: 1}, + SemiPos: token.Pos{Column: 23, Line: 1}, + Expression: &ast.StringLiteral{ + Type: token.NonEmptyStringLiteral, + Position: token.Pos{Column: 8, Line: 1}, + Value: `"Hello World!!"`, + }, + }, + err: nil, + }, + { + name: "Not found return keyword", + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{Column: 1, Line: 1}, + Msg: "not found return keyword.", + }, + }, + { + name: "Not found expression", + input: "return pragma", + err: &token.PosError{ + Pos: token.Pos{Column: 8, Line: 1}, + Msg: "not found expression.", + }, + }, + { + name: "Not found semicolon.", + input: `return "test" pragma`, + err: &token.PosError{ + Pos: token.Pos{Column: 15, Line: 1}, + Msg: "not found semicolon.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseReturnStatement() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} diff --git a/symbolAliases.go b/symbolAliases.go new file mode 100644 index 0000000..d9a96ad --- /dev/null +++ b/symbolAliases.go @@ -0,0 +1,96 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseSymbolAlias() (*ast.SymbolAlias, error) { + smbl, err := p.ParseIdentifier() + if err != nil { + return nil, err + } + + as, err := p.lexer.Peek() + if err != nil { + return nil, err + } + if as.Type != token.As { + return &ast.SymbolAlias{Symbol: smbl}, nil + } + if _, err := p.lexer.Scan(); err != nil { + return nil, err + } + + alias, err := p.ParseIdentifier() + if err != nil { + return nil, err + } + + return &ast.SymbolAlias{ + Symbol: smbl, + As: &as.Position, + Alias: &alias, + }, nil +} + +func (p *Parser) ParseSymbolAliases() (*ast.SymbolAliases, error) { + lbrace, err := p.lexer.Peek() + if err != nil { + return nil, err + } + + if lbrace.Type != token.LBrace { + return nil, token.NewPosError(lbrace.Position, "not found LBrace.") + } + + if _, err := p.lexer.Scan(); err != nil { + return nil, err + } + + alias, err := p.ParseSymbolAlias() + if err != nil { + return nil, err + } + + aliases := []*ast.SymbolAlias{alias} + commas := make([]*token.Pos, 0) + + for { + comma, err := p.lexer.Peek() + if err != nil { + return nil, err + } + if comma.Type != token.Comma { + break + } + + if _, err := p.lexer.Scan(); err != nil { + return nil, err + } + + alias, err := p.ParseSymbolAlias() + if err != nil { + return nil, err + } + + aliases = append(aliases, alias) + commas = append(commas, &comma.Position) + } + + rbrace, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + if rbrace.Type != token.RBrace { + return nil, token.NewPosError(rbrace.Position, "not found RBrace.") + } + + return &ast.SymbolAliases{ + LBrace: lbrace.Position, + Aliases: aliases, + Commas: commas, + RBrace: rbrace.Position, + }, nil +} diff --git a/symbolAliases_test.go b/symbolAliases_test.go new file mode 100644 index 0000000..67a9d71 --- /dev/null +++ b/symbolAliases_test.go @@ -0,0 +1,144 @@ +package solparser_test + +import ( + "strings" + "testing" + + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseSymbolAlias(t *testing.T) { + tests := []struct { + input string + want *ast.SymbolAlias + err *token.PosError + }{ + { + input: "symbol as alias1}", + want: &ast.SymbolAlias{ + Symbol: ast.Identifier(tkn(token.Identifier, "symbol", pos(1, 1))), + As: posPtr(8, 1), + Alias: &ast.Identifier{ + Type: token.Identifier, + Value: "alias1", + Position: pos(11, 1), + }, + }, + }, + { + input: "symbol}", + want: &ast.SymbolAlias{ + Symbol: ast.Identifier(tkn(token.Identifier, "symbol", pos(1, 1))), + As: nil, + Alias: nil, + }, + }, + { + input: "pragma", + err: perr(pos(1, 1), "keyword is not available as identifier."), + }, + { + input: "symbol as pragma", + err: perr(pos(11, 1), "keyword is not available as identifier."), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.input, func(t *testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseSymbolAlias() + assert(t, got, tt.want, err, tt.err) + }) + } +} + +func TestParser_ParseSymbolAliases(t *testing.T) { + tests := []struct { + input string + want *ast.SymbolAliases + err *token.PosError + }{ + { + input: "{symbol1}", + want: &ast.SymbolAliases{ + LBrace: pos(1, 1), + Aliases: []*ast.SymbolAlias{ + { + Symbol: ast.Identifier(tkn(token.Identifier, "symbol1", pos(2, 1))), + }, + }, + Commas: []*token.Pos{}, + RBrace: pos(9, 1), + }, + }, + { + input: "{symbol2 as alias1}", + want: &ast.SymbolAliases{ + LBrace: pos(1, 1), + Aliases: []*ast.SymbolAlias{ + { + Symbol: ast.Identifier(tkn(token.Identifier, "symbol2", pos(2, 1))), + As: posPtr(10, 1), + Alias: &ast.Identifier{ + Type: token.Identifier, + Value: "alias1", + Position: pos(13, 1), + }, + }, + }, + Commas: []*token.Pos{}, + RBrace: pos(19, 1), + }, + }, + { + input: "{symbol1, symbol2 as alias1}", + want: &ast.SymbolAliases{ + LBrace: pos(1, 1), + Aliases: []*ast.SymbolAlias{ + { + Symbol: ast.Identifier(tkn(token.Identifier, "symbol1", pos(2, 1))), + }, + { + Symbol: ast.Identifier(tkn(token.Identifier, "symbol2", pos(11, 1))), + As: posPtr(19, 1), + Alias: &ast.Identifier{ + Type: token.Identifier, + Value: "alias1", + Position: pos(22, 1), + }, + }, + }, + Commas: []*token.Pos{posPtr(9, 1)}, + RBrace: pos(28, 1), + }, + }, + { + input: "symbol1, symbol2 as alian1}", + err: perr(pos(1, 1), "not found LBrace."), + }, + { + input: "{symbol1, as alian1", + err: perr(pos(11, 1), "keyword is not available as identifier."), + }, + { + input: "{symbol1, symbol2 as alian1", + err: perr(pos(28, 1), "not found RBrace."), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.input, func(t *testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseSymbolAliases() + assert(t, got, tt.want, err, tt.err) + }) + } +} diff --git a/token/error.go b/token/error.go new file mode 100644 index 0000000..9994b95 --- /dev/null +++ b/token/error.go @@ -0,0 +1,22 @@ +package token + +type PosError struct { + Pos Pos + Msg string +} + +var _ error = &PosError{} + +func NewPosError(pos Pos, msg string) *PosError { + return &PosError{ + Pos: pos, + Msg: msg, + } +} + +func (e *PosError) Error() string { + if e.Pos.IsValid() { + return e.Pos.String() + ": " + e.Msg + } + return e.Msg +} diff --git a/error_test.go b/token/error_test.go similarity index 79% rename from error_test.go rename to token/error_test.go index 1d31ca9..5da487d 100644 --- a/error_test.go +++ b/token/error_test.go @@ -1,21 +1,20 @@ -package solparser_test +package token_test import ( "testing" - "github.com/uji/solparser" "github.com/uji/solparser/token" ) -func TestErrorError(t *testing.T) { +func TestError_PosError(t *testing.T) { tests := []struct { name string - err solparser.Error + err token.PosError want string }{ { name: "normal case", - err: solparser.Error{ + err: token.PosError{ Pos: token.Pos{ Column: 1, Line: 1, @@ -26,7 +25,7 @@ func TestErrorError(t *testing.T) { }, { name: "invalid position case", - err: solparser.Error{ + err: token.PosError{ Pos: token.Pos{ Column: 0, Line: 1, diff --git a/token/token.go b/token/token.go index 83d1ff9..9e98de3 100644 --- a/token/token.go +++ b/token/token.go @@ -1,12 +1,17 @@ package token -import "fmt" +import ( + "fmt" + + "github.com/SteelSeries/bufrr" +) type TokenType int const ( Invalid TokenType = iota - Unknown + + EOS // End of source indicator. LParen // ( RParen // ) @@ -61,7 +66,7 @@ const ( Inc // ++ Dec // -- DoubleQuote // " - SingleQuote // ' + SingleQuote // ' or \' // Reserved Keyword After @@ -120,13 +125,11 @@ const ( Event External Fallback - False Fixed For From Function Global - Hex If Immutable Import @@ -152,17 +155,34 @@ const ( Storage String Struct - True Try Type Using View Virtual While + + HexString + + // Literal + TrueLiteral + FalseLiteral + Number + NonEmptyStringLiteral + EmptyStringLiteral + UnicodeStringLiteral + CommentLiteral + + // Not keywords or future reserved words + Identifier ) +var EOSString string = string([]rune{bufrr.EOF}) + func asKeyword(str string) TokenType { switch str { + case EOSString: + return EOS case "(": return LParen case ")": @@ -374,7 +394,7 @@ func asKeyword(str string) TokenType { case "fallback": return Fallback case "false": - return False + return FalseLiteral case "fixed": return Fixed case "for": @@ -385,8 +405,6 @@ func asKeyword(str string) TokenType { return Function case "global": return Global - case "hex": - return Hex case "if": return If case "immutable": @@ -438,7 +456,7 @@ func asKeyword(str string) TokenType { case "struct": return Struct case "true": - return True + return TrueLiteral case "try": return Try case "type": @@ -453,7 +471,7 @@ func asKeyword(str string) TokenType { return While } - return Unknown + return Identifier } func asToken(str string) TokenType { @@ -461,16 +479,16 @@ func asToken(str string) TokenType { } type Token struct { - TokenType TokenType - Text string - Pos Pos + Type TokenType + Value string + Position Pos } func NewToken(ch string, pos Pos) Token { return Token{ - TokenType: asToken(ch), - Text: ch, - Pos: pos, + Type: asToken(ch), + Value: ch, + Position: pos, } } @@ -488,3 +506,28 @@ func (p Pos) String() string { return fmt.Sprintf("%d:%d", p.Line, p.Column) } + +// IsSpace reports whether the character is a Unicode white space character. +// We avoid dependency on the unicode package, but check validity of the implementation +// in the tests. +func IsSpace(r rune) bool { + if r <= '\u00FF' { + // Obvious ASCII ones: \t through \r plus space. Plus two Latin-1 oddballs. + switch r { + case ' ', '\n', '\t', '\v', '\f', '\r': + return true + case '\u0085', '\u00A0': + return true + } + return false + } + // High-valued ones. + if '\u2000' <= r && r <= '\u200a' { + return true + } + switch r { + case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000': + return true + } + return false +} diff --git a/typeName.go b/typeName.go new file mode 100644 index 0000000..704017c --- /dev/null +++ b/typeName.go @@ -0,0 +1,46 @@ +package solparser + +import ( + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func (p *Parser) ParseTypeName() (ast.TypeName, error) { + tkn, err := p.lexer.Peek() + if err != nil { + return nil, err + } + + switch tkn.Type { + case token.Address, token.String, token.Bytes, token.Fixed, token.Bool: + return p.ParseElementaryTypeName() + } + + return nil, token.NewPosError(tkn.Position, "not found type-name.") +} + +func (p *Parser) ParseElementaryTypeName() (ast.TypeName, error) { + tkn, err := p.lexer.Scan() + if err != nil { + return nil, err + } + + if tkn.Type == token.Address { + tkn2, err := p.lexer.Peek() + if err != nil { + return nil, err + } + + if tkn2.Type == token.Payable { + p.lexer.Scan() + return ast.ElementaryTypeName{&tkn, &tkn2}, nil + } + } + + switch tkn.Type { + case token.Address, token.String, token.Bytes, token.Fixed, token.Bool: + return ast.ElementaryTypeName{&tkn}, nil + } + + return nil, token.NewPosError(tkn.Position, "not found elementary type name keyword.") +} diff --git a/typeName_test.go b/typeName_test.go new file mode 100644 index 0000000..20d4e3f --- /dev/null +++ b/typeName_test.go @@ -0,0 +1,135 @@ +package solparser_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/uji/solparser" + "github.com/uji/solparser/ast" + "github.com/uji/solparser/token" +) + +func TestParser_ParseTypeName(t *testing.T) { + tests := []struct { + name string + input string + want ast.TypeName + err *token.PosError + }{ + { + name: "ElementaryTypeName", + input: "string)", + want: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + }, + { + name: "Not TypeName", + input: "pragma", + err: &token.PosError{ + Pos: token.Pos{Column: 1, Line: 1}, + Msg: "not found type-name.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseTypeName() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +} + +func TestParser_ParseElementaryTypeName(t *testing.T) { + tests := []struct { + input string + want ast.TypeName + err *token.PosError + }{ + { + input: "address", + want: ast.ElementaryTypeName{ + { + Type: token.Address, + Value: "address", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + }, + { + input: "bool", + want: ast.ElementaryTypeName{ + { + Type: token.Bool, + Value: "bool", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + }, + { + input: "string", + want: ast.ElementaryTypeName{ + { + Type: token.String, + Value: "string", + Position: token.Pos{Column: 1, Line: 1}, + }, + }, + }, + { + input: "address payable", + want: ast.ElementaryTypeName{ + { + Type: token.Address, + Value: "address", + Position: token.Pos{Column: 1, Line: 1}, + }, + { + Type: token.Payable, + Value: "payable", + Position: token.Pos{Column: 9, Line: 1}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(*testing.T) { + r := strings.NewReader(tt.input) + p := solparser.New(r) + + got, err := p.ParseElementaryTypeName() + + var sErr *token.PosError + if errors.As(err, &sErr) { + if diff := cmp.Diff(tt.err, sErr); diff != "" { + t.Errorf("%s", diff) + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("%s", diff) + } + }) + } +}