github.com/elves/elvish@v0.15.0/pkg/parse/parse_test.go (about)

     1  package parse
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"testing"
     7  )
     8  
     9  func a(c ...interface{}) ast {
    10  	// Shorthand used for checking Compound and levels beneath.
    11  	return ast{"Chunk/Pipeline/Form", fs{"Head": "a", "Args": c}}
    12  }
    13  
    14  var goodCases = []struct {
    15  	src string
    16  	ast ast
    17  }{
    18  	// Chunk
    19  	// Smoke test.
    20  	{"a;b;c\n;d", ast{"Chunk", fs{"Pipelines": []string{"a", "b", "c", "d"}}}},
    21  	// Empty chunk should have Pipelines=nil.
    22  	{"", ast{"Chunk", fs{"Pipelines": nil}}},
    23  	// Superfluous newlines and semicolons should not result in empty
    24  	// pipelines.
    25  	{"  ;\n\n  ls \t ;\n", ast{"Chunk", fs{"Pipelines": []string{"ls \t "}}}},
    26  
    27  	// Pipeline
    28  	{"a|b|c|d", ast{
    29  		"Chunk/Pipeline", fs{"Forms": []string{"a", "b", "c", "d"}}}},
    30  	// Newlines are allowed after pipes.
    31  	{"a| \n \n b", ast{
    32  		"Chunk/Pipeline", fs{"Forms": []string{"a", "b"}}}},
    33  
    34  	// Form
    35  	// Smoke test.
    36  	{"ls x y", ast{"Chunk/Pipeline/Form", fs{
    37  		"Head": "ls",
    38  		"Args": []string{"x", "y"}}}},
    39  	// Assignments.
    40  	{"k=v k[a][b]=v {a,b[1]}=(ha)", ast{"Chunk/Pipeline/Form", fs{
    41  		"Assignments": []string{"k=v", "k[a][b]=v", "{a,b[1]}=(ha)"}}}},
    42  	// Temporary assignment.
    43  	{"k=v k[a][b]=v a", ast{"Chunk/Pipeline/Form", fs{
    44  		"Assignments": []string{"k=v", "k[a][b]=v"},
    45  		"Head":        "a"}}},
    46  	// Redirections
    47  	{"a >b", ast{"Chunk/Pipeline/Form", fs{
    48  		"Head": "a",
    49  		"Redirs": []ast{
    50  			{"Redir", fs{"Mode": Write, "Right": "b"}}},
    51  	}}},
    52  	// More redirections
    53  	{"a >>b 2>b 3>&- 4>&1 5<c 6<>d", ast{"Chunk/Pipeline/Form", fs{
    54  		"Head": "a",
    55  		"Redirs": []ast{
    56  			{"Redir", fs{"Mode": Append, "Right": "b"}},
    57  			{"Redir", fs{"Left": "2", "Mode": Write, "Right": "b"}},
    58  			{"Redir", fs{"Left": "3", "Mode": Write, "RightIsFd": true, "Right": "-"}},
    59  			{"Redir", fs{"Left": "4", "Mode": Write, "RightIsFd": true, "Right": "1"}},
    60  			{"Redir", fs{"Left": "5", "Mode": Read, "Right": "c"}},
    61  			{"Redir", fs{"Left": "6", "Mode": ReadWrite, "Right": "d"}},
    62  		},
    63  	}}},
    64  	// Options (structure of MapPair tested below with map)
    65  	{"a &a=1 x &b=2", ast{"Chunk/Pipeline/Form", fs{
    66  		"Head": "a",
    67  		"Args": []string{"x"},
    68  		"Opts": []string{"&a=1", "&b=2"},
    69  	}}},
    70  
    71  	// Compound
    72  	{`a b"foo"?$c*'xyz'`, a(ast{"Compound", fs{
    73  		"Indexings": []string{"b", `"foo"`, "?", "$c", "*", "'xyz'"}}})},
    74  
    75  	// Indexing
    76  	{"a $b[c][d][\ne\n]", a(ast{"Compound/Indexing", fs{
    77  		"Head": "$b", "Indicies": []string{"c", "d", "\ne\n"},
    78  	}})},
    79  
    80  	// Primary
    81  
    82  	// Bareword.
    83  	{"a foo", a(ast{"Compound/Indexing/Primary", fs{
    84  		"Type": Bareword, "Value": "foo",
    85  	}})},
    86  
    87  	// Bareword, with all allowed symbols.
    88  	{"a ./\\@%+!=,", a(ast{"Compound/Indexing/Primary", fs{
    89  		"Type": Bareword, "Value": "./\\@%+!=,",
    90  	}})},
    91  
    92  	// Single quote
    93  	{"a '''x''y'''", a(ast{"Compound/Indexing/Primary", fs{
    94  		"Type": SingleQuoted, "Value": "'x'y'",
    95  	}})},
    96  	// Double quote
    97  	{`a "[\c?\c@\cI\^I\^[]"`, // control char sequences
    98  		a(ast{"Compound/Indexing/Primary", fs{
    99  			"Type":  DoubleQuoted,
   100  			"Value": "[\x7f\x00\t\t\x1b]",
   101  		}})},
   102  
   103  	{`a "[\n\t\a\v\\\"]"`, // single char sequences
   104  		a(ast{"Compound/Indexing/Primary", fs{
   105  			"Type":  DoubleQuoted,
   106  			"Value": "[\n\t\a\v\\\"]",
   107  		}})},
   108  
   109  	{`a "b\^[\x1b\u548c\U0002CE23\123\n\t\\"`, // numeric sequences
   110  		a(ast{"Compound/Indexing/Primary", fs{
   111  			"Type":  DoubleQuoted,
   112  			"Value": "b\x1b\x1b\u548c\U0002CE23\123\n\t\\",
   113  		}})},
   114  	// Wildcard
   115  	{"a * ? ** ??", a(
   116  		ast{"Compound/Indexing/Primary", fs{"Type": Wildcard, "Value": "*"}},
   117  		ast{"Compound/Indexing/Primary", fs{"Type": Wildcard, "Value": "?"}},
   118  		ast{"Compound/Indexing/Primary", fs{"Type": Wildcard, "Value": "**"}},
   119  		ast{"Compound", fs{"Indexings": []string{"?", "?"}}},
   120  	)},
   121  	// Variable
   122  	{`a $x $'!@#' $"\n"`, a(
   123  		ast{"Compound/Indexing/Primary", fs{"Type": Variable, "Value": "x"}},
   124  		ast{"Compound/Indexing/Primary", fs{"Type": Variable, "Value": "!@#"}},
   125  		ast{"Compound/Indexing/Primary", fs{"Type": Variable, "Value": "\n"}},
   126  	)},
   127  	// List
   128  	{"a [] [ ] [1] [ 2] [3 ] [\n 4 \n5\n 6 7 \n]", a(
   129  		ast{"Compound/Indexing/Primary", fs{
   130  			"Type":     List,
   131  			"Elements": []ast{}}},
   132  		ast{"Compound/Indexing/Primary", fs{
   133  			"Type":     List,
   134  			"Elements": []ast{}}},
   135  		ast{"Compound/Indexing/Primary", fs{
   136  			"Type":     List,
   137  			"Elements": []string{"1"}}},
   138  		ast{"Compound/Indexing/Primary", fs{
   139  			"Type":     List,
   140  			"Elements": []string{"2"}}},
   141  		ast{"Compound/Indexing/Primary", fs{
   142  			"Type":     List,
   143  			"Elements": []string{"3"}}},
   144  		ast{"Compound/Indexing/Primary", fs{
   145  			"Type":     List,
   146  			"Elements": []string{"4", "5", "6", "7"}}},
   147  	)},
   148  	// Map
   149  	{"a [&k=v] [ &k=v] [&k=v ] [ &k=v ] [ &k= v] [&k= \n v] [\n&a=b &c=d \n &e=f\n\n]", a(
   150  		ast{"Compound/Indexing/Primary", fs{
   151  			"Type":     Map,
   152  			"MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}},
   153  		ast{"Compound/Indexing/Primary", fs{
   154  			"Type":     Map,
   155  			"MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}},
   156  		ast{"Compound/Indexing/Primary", fs{
   157  			"Type":     Map,
   158  			"MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}},
   159  		ast{"Compound/Indexing/Primary", fs{
   160  			"Type":     Map,
   161  			"MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}},
   162  		ast{"Compound/Indexing/Primary", fs{
   163  			"Type":     Map,
   164  			"MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}},
   165  		ast{"Compound/Indexing/Primary", fs{
   166  			"Type":     Map,
   167  			"MapPairs": []ast{{"MapPair", fs{"Key": "k", "Value": "v"}}}}},
   168  		ast{"Compound/Indexing/Primary", fs{
   169  			"Type": Map,
   170  			"MapPairs": []ast{
   171  				{"MapPair", fs{"Key": "a", "Value": "b"}},
   172  				{"MapPair", fs{"Key": "c", "Value": "d"}},
   173  				{"MapPair", fs{"Key": "e", "Value": "f"}},
   174  			}}},
   175  	)},
   176  	// Empty map
   177  	{"a [&] [ &] [& ] [ & ]", a(
   178  		ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}},
   179  		ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}},
   180  		ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}},
   181  		ast{"Compound/Indexing/Primary", fs{"Type": Map, "MapPairs": nil}},
   182  	)},
   183  	// Lambda
   184  	{"a []{} [ ]{ } []{ echo 233 } [ x y ]{puts $x $y} { put haha}", a(
   185  		ast{"Compound/Indexing/Primary", fs{
   186  			"Type": Lambda, "Elements": []ast{}, "Chunk": "",
   187  		}},
   188  		ast{"Compound/Indexing/Primary", fs{
   189  			"Type": Lambda, "Elements": []ast{}, "Chunk": " ",
   190  		}},
   191  		ast{"Compound/Indexing/Primary", fs{
   192  			"Type": Lambda, "Elements": []ast{}, "Chunk": " echo 233 ",
   193  		}},
   194  		ast{"Compound/Indexing/Primary", fs{
   195  			"Type": Lambda, "Elements": []string{"x", "y"}, "Chunk": "puts $x $y",
   196  		}},
   197  		ast{"Compound/Indexing/Primary", fs{
   198  			"Type": Lambda, "Elements": []ast{}, "Chunk": " put haha",
   199  		}},
   200  	)},
   201  	// Lambda with arguments and options
   202  	{"a [a b &k=v]{}", a(
   203  		ast{"Compound/Indexing/Primary", fs{
   204  			"Type":     Lambda,
   205  			"Elements": []string{"a", "b"},
   206  			"MapPairs": []string{"&k=v"},
   207  			"Chunk":    "",
   208  		}},
   209  	)},
   210  	// Output capture
   211  	{"a () (b;c) (c\nd)", a(
   212  		ast{"Compound/Indexing/Primary", fs{
   213  			"Type": OutputCapture, "Chunk": ""}},
   214  		ast{"Compound/Indexing/Primary", fs{
   215  			"Type": OutputCapture, "Chunk": ast{
   216  				"Chunk", fs{"Pipelines": []string{"b", "c"}},
   217  			}}},
   218  		ast{"Compound/Indexing/Primary", fs{
   219  			"Type": OutputCapture, "Chunk": ast{
   220  				"Chunk", fs{"Pipelines": []string{"c", "d"}},
   221  			}}},
   222  	)},
   223  	// Exitus capture
   224  	{"a ?() ?(b;c)", a(
   225  		ast{"Compound/Indexing/Primary", fs{
   226  			"Type": ExceptionCapture, "Chunk": ""}},
   227  		ast{"Compound/Indexing/Primary", fs{
   228  			"Type": ExceptionCapture, "Chunk": "b;c",
   229  		}})},
   230  	// Braced
   231  	{"a {,a,c\ng\n}", a(
   232  		ast{"Compound/Indexing/Primary", fs{
   233  			"Type":   Braced,
   234  			"Braced": []string{"", "a", "c", "g", ""}}})},
   235  	// Tilde
   236  	{"a ~xiaq/go", a(
   237  		ast{"Compound", fs{
   238  			"Indexings": []ast{
   239  				{"Indexing/Primary", fs{"Type": Tilde, "Value": "~"}},
   240  				{"Indexing/Primary", fs{"Type": Bareword, "Value": "xiaq/go"}},
   241  			},
   242  		}},
   243  	)},
   244  	// Tilde and wildcard
   245  	{"a ~xiaq/*.go", a(
   246  		ast{"Compound", fs{
   247  			"Indexings": []ast{
   248  				{"Indexing/Primary", fs{"Type": Tilde, "Value": "~"}},
   249  				{"Indexing/Primary", fs{"Type": Bareword, "Value": "xiaq/"}},
   250  				{"Indexing/Primary", fs{"Type": Wildcard, "Value": "*"}},
   251  				{"Indexing/Primary", fs{"Type": Bareword, "Value": ".go"}},
   252  			},
   253  		}},
   254  	)},
   255  
   256  	// Line continuation: "^\n" is considered whitespace
   257  	{"a b^\nc", ast{
   258  		"Chunk/Pipeline/Form", fs{"Head": "a", "Args": []string{"b", "c"}}}},
   259  
   260  	// Carriage returns are normally treated the same as newlines:
   261  	// Separating pipelines in a chunk
   262  	{"a\rb", ast{"Chunk", fs{"Pipelines": []string{"a", "b"}}}},
   263  	{"a\r\nb", ast{"Chunk", fs{"Pipelines": []string{"a", "b"}}}},
   264  	// Whitespace padding in lambdas
   265  	{"a { \rfoo\r\nbar }", a(
   266  		ast{"Compound/Indexing/Primary",
   267  			fs{"Type": Lambda, "Chunk": " \rfoo\r\nbar "}},
   268  	)},
   269  	// Separating elements in lists
   270  	{"a [a\rb]", a(
   271  		ast{"Compound/Indexing/Primary", fs{
   272  			"Type":     List,
   273  			"Elements": []string{"a", "b"}}})},
   274  
   275  	// However, in line continuations, \r\n is treated as a single newline
   276  	{"a b^\r\nc", ast{
   277  		"Chunk/Pipeline/Form", fs{"Head": "a", "Args": []string{"b", "c"}}}},
   278  	// But a lone \r also works
   279  	{"a b^\rc", ast{
   280  		"Chunk/Pipeline/Form", fs{"Head": "a", "Args": []string{"b", "c"}}}},
   281  
   282  	// Comments in chunks.
   283  	{"a#haha\nb#lala", ast{
   284  		"Chunk", fs{"Pipelines": []ast{
   285  			{"Pipeline/Form", fs{"Head": "a"}},
   286  			{"Pipeline/Form", fs{"Head": "b"}},
   287  		}}}},
   288  	// Comments in lists.
   289  	{"a [a#haha\nb]", a(
   290  		ast{"Compound/Indexing/Primary", fs{
   291  			"Type":     List,
   292  			"Elements": []string{"a", "b"},
   293  		}},
   294  	)},
   295  }
   296  
   297  func TestParse(t *testing.T) {
   298  	for _, tc := range goodCases {
   299  		src := SourceForTest(tc.src)
   300  		tree, err := Parse(src)
   301  		if err != nil {
   302  			t.Errorf("Parse(%q) returns error: %v", tc.src, err)
   303  		}
   304  		if tree.Source != src {
   305  			t.Errorf("Parse(%q) returns source %v, want %v", tc.src, tree.Source, src)
   306  		}
   307  		err = checkParseTree(tree.Root)
   308  		if err != nil {
   309  			t.Errorf("Parse(%q) returns bad parse tree: %v", tc.src, err)
   310  			fmt.Fprintf(os.Stderr, "Parse tree of %q:\n", tc.src)
   311  			pprintParseTree(tree.Root, os.Stderr)
   312  		}
   313  		err = checkAST(tree.Root, tc.ast)
   314  		if err != nil {
   315  			t.Errorf("Parse(%q) returns bad AST: %v", tc.src, err)
   316  			fmt.Fprintf(os.Stderr, "AST of %q:\n", tc.src)
   317  			pprintAST(tree.Root, os.Stderr)
   318  		}
   319  	}
   320  }
   321  
   322  var parseErrorTests = []struct {
   323  	src      string
   324  	errPart  string
   325  	errAtEnd bool
   326  	errMsg   string
   327  }{
   328  	// Empty form.
   329  	{src: "a|", errAtEnd: true, errMsg: "should be form"},
   330  	// Unopened parens.
   331  	{src: ")", errPart: ")", errMsg: "unexpected rune ')'"},
   332  	{src: "]", errPart: "]", errMsg: "unexpected rune ']'"},
   333  	{src: "}", errPart: "}", errMsg: "unexpected rune '}'"},
   334  	// Unclosed parens.
   335  	{src: "a (", errAtEnd: true, errMsg: "should be ')'"},
   336  	{src: "a [", errAtEnd: true, errMsg: "should be ']'"},
   337  	{src: "a {", errAtEnd: true, errMsg: "should be ',' or '}'"},
   338  	// Bogus ampersand in form.
   339  	{src: "a & &", errPart: "&", errMsg: "unexpected rune '&'"},
   340  	// No redirection source.
   341  	{src: "a >", errAtEnd: true, errMsg: "should be a composite term representing filename"},
   342  	{src: "a >&", errAtEnd: true, errMsg: "should be a composite term representing fd"},
   343  	// Unmatched paren in indexing.
   344  	{src: "a $a[0}", errPart: "}", errMsg: "should be ']'"},
   345  	// Unterminated string.
   346  	{src: "'a", errAtEnd: true, errMsg: "string not terminated"},
   347  	{src: `"a`, errAtEnd: true, errMsg: "string not terminated"},
   348  	// Bad escape sequence.
   349  	{src: `a "\^` + "\t", errPart: "\t",
   350  		errMsg: "invalid control sequence, should be a codepoint between 0x3F and 0x5F"},
   351  	{src: `a "\xQQ"`, errPart: "Q", errMsg: "invalid escape sequence, should be hex digit"},
   352  	{src: `a "\1ab"`, errPart: "a", errMsg: "invalid escape sequence, should be octal digit"},
   353  	{src: `a "\i"`, errPart: "i", errMsg: "invalid escape sequence"},
   354  	// Unterminated variable name.
   355  	{src: "$", errAtEnd: true, errMsg: "should be variable name"},
   356  	// Unmatched (.
   357  	{src: "a (", errAtEnd: true, errMsg: "should be ')'"},
   358  	// List-map hybrid.
   359  	// TODO(xiaq): Add correct position information.
   360  	{src: "a [a &k=v]", errAtEnd: true, errMsg: "cannot contain both list elements and map pairs"},
   361  	// Unmatched {.
   362  	{src: "{ a", errAtEnd: true, errMsg: "should be '}'"},
   363  	// Unfinished line continuation.
   364  	{src: `a ^`, errAtEnd: true, errMsg: "should be newline"},
   365  }
   366  
   367  func TestParseError(t *testing.T) {
   368  	for _, test := range parseErrorTests {
   369  		t.Run(test.src, func(t *testing.T) {
   370  			_, err := Parse(SourceForTest(test.src))
   371  			if err == nil {
   372  				t.Fatalf("no error")
   373  			}
   374  			parseError := err.(*Error).Entries[0]
   375  			r := parseError.Context
   376  
   377  			if errPart := test.src[r.From:r.To]; errPart != test.errPart {
   378  				t.Errorf("err part is %q, want %q", errPart, test.errPart)
   379  			}
   380  			if errAtEnd := r.From == len(test.src); errAtEnd != test.errAtEnd {
   381  				t.Errorf("err at end is %v, want %v", errAtEnd, test.errAtEnd)
   382  			}
   383  			if errMsg := parseError.Message; errMsg != test.errMsg {
   384  				t.Errorf("err message is %q, want %q", errMsg, test.errMsg)
   385  			}
   386  		})
   387  	}
   388  }