gUnit.g revision 324c4644fee44b9898524c09511bd33c3f12e2df
1/*
2[The "BSD licence"]
3Copyright (c) 2007-2008 Leon Jen-Yuan Su
4All rights reserved.
5
6Redistribution and use in source and binary forms, with or without
7modification, are permitted provided that the following conditions
8are met:
9
10 1. Redistributions of source code must retain the above copyright
11    notice, this list of conditions and the following disclaimer.
12 2. Redistributions in binary form must reproduce the above copyright
13    notice, this list of conditions and the following disclaimer in the
14    documentation and/or other materials provided with the distribution.
15 3. The name of the author may not be used to endorse or promote products
16    derived from this software without specific prior written permission.
17
18THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
22INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28*/
29grammar gUnit;
30options {language=Java;}
31tokens {
32	OK = 'OK';
33	FAIL = 'FAIL';
34	DOC_COMMENT;
35}
36@header {package org.antlr.gunit;}
37@lexer::header {
38package org.antlr.gunit;
39import java.io.BufferedReader;
40import java.io.IOException;
41import java.io.StringReader;
42}
43@members {
44public GrammarInfo grammarInfo;
45public gUnitParser(TokenStream input, GrammarInfo grammarInfo) {
46	super(input);
47	this.grammarInfo = grammarInfo;
48}
49}
50
51gUnitDef:	'gunit' g1=id ('walks' g2=id)? ';'
52		{
53		if ( $g2.text!=null ) {
54			grammarInfo.setGrammarName($g2.text);
55			grammarInfo.setTreeGrammarName($g1.text);
56		}
57		else {
58			grammarInfo.setGrammarName($g1.text);
59		}
60		}
61		optionsSpec? header? testsuite*
62	;
63
64optionsSpec
65	:	OPTIONS (option ';')+ '}'
66	;
67
68// Note: currently, this is the only valid option for setting customized tree adaptor
69option	:	id '=' treeAdaptor
70		{
71		if ( $id.text.equals("TreeAdaptor") ) {
72		    grammarInfo.setAdaptor($treeAdaptor.text);
73		}
74		// TODO: need a better error logging strategy
75		else System.err.println("Invalid option detected: "+$text);
76		}
77	;
78
79treeAdaptor
80	:	id EXT*
81	;
82
83header	:	'@header' ACTION
84		{
85		int pos1, pos2;
86		if ( (pos1=$ACTION.text.indexOf("package"))!=-1 && (pos2=$ACTION.text.indexOf(';'))!=-1 ) {
87			grammarInfo.setGrammarPackage($ACTION.text.substring(pos1+8, pos2).trim());	// substring the package path
88		}
89		else {
90			System.err.println("error(line "+$ACTION.getLine()+"): invalid header");
91		}
92		}
93	;
94
95testsuite	// gUnit test suite based on individual rule
96scope {
97boolean isLexicalRule;
98}
99@init {
100gUnitTestSuite ts = null;
101$testsuite::isLexicalRule = false;
102}
103	:	(	r1=RULE_REF ('walks' r2=RULE_REF)?
104			{
105			if ( $r2==null ) ts = new gUnitTestSuite($r1.text);
106			else ts = new gUnitTestSuite($r1.text, $r2.text);
107			}
108		|	t=TOKEN_REF
109			{
110			ts = new gUnitTestSuite();
111			ts.setLexicalRuleName($t.text);
112			$testsuite::isLexicalRule = true;
113			}
114		)
115		':'
116		testcase[ts]+ {grammarInfo.addRuleTestSuite(ts);}
117	;
118
119// TODO : currently gUnit just ignores illegal test for lexer rule, but should also emit a reminding message
120testcase[gUnitTestSuite ts]	// individual test within a (rule)testsuite
121	:	input expect {$ts.addTestCase($input.in, $expect.out);}
122	;
123
124input returns [gUnitTestInput in]
125@init {
126String testInput = null;
127boolean inputIsFile = false;
128int line = -1;
129}
130@after {
131in = new gUnitTestInput(testInput, inputIsFile, line);
132}
133	:	STRING
134		{
135		testInput = $STRING.text.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t")
136		.replace("\\b", "\b").replace("\\f", "\f").replace("\\\"", "\"").replace("\\'", "\'").replace("\\\\", "\\");
137		line = $STRING.line;
138		}
139	|	ML_STRING
140		{
141		testInput = $ML_STRING.text;
142		line = $ML_STRING.line;
143		}
144	|	file
145		{
146		testInput = $file.text;
147		inputIsFile = true;
148		line = $file.line;
149		}
150	;
151
152expect returns [AbstractTest out]
153	:	OK {$out = new BooleanTest(true);}
154	|	FAIL {$out = new BooleanTest(false);}
155	|	'returns' RETVAL {if ( !$testsuite::isLexicalRule ) $out = new ReturnTest($RETVAL);}
156	|	'->' output {if ( !$testsuite::isLexicalRule ) $out = new OutputTest($output.token);}
157	;
158
159output returns [Token token]
160	:	STRING
161		{
162		$STRING.setText($STRING.text.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t")
163		.replace("\\b", "\b").replace("\\f", "\f").replace("\\\"", "\"").replace("\\'", "\'").replace("\\\\", "\\"));
164		$token = $STRING;
165		}
166	|	ML_STRING {$token = $ML_STRING;}
167	|	AST {$token = $AST;}
168	|	ACTION {$token = $ACTION;}
169	;
170
171file returns [int line]
172	:	id EXT? {$line = $id.line;}
173	;
174
175id returns [int line]
176	:	TOKEN_REF {$line = $TOKEN_REF.line;}
177	|	RULE_REF {$line = $RULE_REF.line;}
178	;
179
180// L E X I C A L   R U L E S
181
182SL_COMMENT
183 	:	'//' ~('\r'|'\n')* '\r'? '\n' {$channel=HIDDEN;}
184	;
185
186ML_COMMENT
187	:	'/*' {$channel=HIDDEN;} .* '*/'
188	;
189
190STRING	:	'"' ( ESC | ~('\\'|'"') )* '"' {setText(getText().substring(1, getText().length()-1));}
191	;
192
193ML_STRING
194	:	{// we need to determine the number of spaces or tabs (indentation) for multi-line input
195		StringBuffer buf = new StringBuffer();
196		int i = -1;
197		int c = input.LA(-1);
198		while ( c==' ' || c=='\t' ) {
199			buf.append((char)c);
200			c = input.LA(--i);
201		}
202		String indentation = buf.reverse().toString();
203		}
204		'<<' .* '>>'
205		{// also determine the appropriate newline separator and get info of the first and last 2 characters (exclude '<<' and '>>')
206		String newline = System.getProperty("line.separator");
207		String front, end;
208		int oldFrontIndex = 2;
209		int oldEndIndex = getText().length()-2;
210		int newFrontIndex, newEndIndex;
211		if ( newline.length()==1 ) {
212			front = getText().substring(2, 3);
213			end = getText().substring(getText().length()-3, getText().length()-2);
214			newFrontIndex = 3;
215			newEndIndex = getText().length()-3;
216		}
217		else {// must be 2, e.g. Windows System which uses \r\n as a line separator
218			front = getText().substring(2, 4);
219			end = getText().substring(getText().length()-4, getText().length()-2);
220			newFrontIndex = 4;
221			newEndIndex = getText().length()-4;
222		}
223		// strip unwanted characters, e.g. '<<' (including a newline after it) or '>>'  (including a newline before it)
224		String temp = null;
225		if ( front.equals(newline) && end.equals(newline) ) {
226			// need to handle the special case: <<\n>> or <<\r\n>>
227			if ( newline.length()==1 && getText().length()==5 ) temp = "";
228			else if ( newline.length()==2 && getText().length()==6 ) temp = "";
229			else temp = getText().substring(newFrontIndex, newEndIndex);
230		}
231		else if ( front.equals(newline) ) {
232			temp = getText().substring(newFrontIndex, oldEndIndex);
233		}
234		else if ( end.equals(newline) ) {
235			temp = getText().substring(oldFrontIndex, newEndIndex);
236		}
237		else {
238			temp = getText().substring(oldFrontIndex, oldEndIndex);
239		}
240		// finally we need to prpcess the indentation line by line
241		BufferedReader bufReader = new BufferedReader(new StringReader(temp));
242		buf = new StringBuffer();
243		String line = null;
244		int count = 0;
245		try {
246			while((line = bufReader.readLine()) != null) {
247				if ( line.startsWith(indentation) ) line = line.substring(indentation.length());
248				if ( count>0 ) buf.append(newline);
249				buf.append(line);
250				count++;
251			}
252			setText(buf.toString());
253		}
254		catch (IOException ioe) {
255			setText(temp);
256		}
257		}
258	;
259
260TOKEN_REF
261	:	'A'..'Z' ('a'..'z'|'A'..'Z'|'_'|'0'..'9')*
262	;
263
264RULE_REF
265	:	'a'..'z' ('a'..'z'|'A'..'Z'|'_'|'0'..'9')*
266	;
267
268EXT	:	'.'('a'..'z'|'A'..'Z'|'0'..'9')+;
269
270RETVAL	:	NESTED_RETVAL {setText(getText().substring(1, getText().length()-1));}
271	;
272
273fragment
274NESTED_RETVAL :
275	'['
276	(	options {greedy=false;}
277	:	NESTED_RETVAL
278	|	.
279	)*
280	']'
281	;
282
283AST	:	NESTED_AST (' '? NESTED_AST)*;
284
285fragment
286NESTED_AST :
287	'('
288	(	NESTED_AST
289	|   STRING_LITERAL
290	|	~('('|')'|'"')
291	)*
292	')'
293	;
294
295OPTIONS	:	'options' WS* '{'
296	;
297
298ACTION
299	:	NESTED_ACTION {setText(getText().substring(1, getText().length()-1));}
300	;
301
302fragment
303NESTED_ACTION :
304	'{'
305	(	options {greedy=false; k=3;}
306	:	NESTED_ACTION
307	|	STRING_LITERAL
308	|	CHAR_LITERAL
309	|	.
310	)*
311	'}'
312	;
313
314fragment
315CHAR_LITERAL
316	:	'\'' ( ESC | ~('\''|'\\') ) '\''
317	;
318
319fragment
320STRING_LITERAL
321	:	'"' ( ESC | ~('\\'|'"') )* '"'
322	;
323
324fragment
325ESC	:	'\\'
326		(	'n'
327		|	'r'
328		|	't'
329		|	'b'
330		|	'f'
331		|	'"'
332		|	'\''
333		|	'\\'
334		|	'>'
335		|	'u' XDIGIT XDIGIT XDIGIT XDIGIT
336		|	. // unknown, leave as it is
337		)
338	;
339
340fragment
341XDIGIT :
342		'0' .. '9'
343	|	'a' .. 'f'
344	|	'A' .. 'F'
345	;
346
347WS	:	(	' '
348		|	'\t'
349		|	'\r'? '\n'
350		)+
351		{$channel=HIDDEN;}
352	;
353