import 'package:petitparser/petitparser.dart'; import 'package:petitparser/reflection.dart'; import 'package:petitparser/src/reflection/internal/linter_rules.dart' as linter_rules; import 'package:petitparser/src/reflection/internal/optimize_rules.dart' as optimize_rules; import 'package:test/test.dart'; import 'utils/matchers.dart'; // Güting, Erwig, Übersetzerbau, Springer (p.63) Map createUebersetzerbau() { final grammar = {}; grammar[#a] = char('a'); grammar[#b] = char('b'); grammar[#c] = char('c'); grammar[#d] = char('d'); grammar[#e] = epsilon(); grammar[#B] = grammar[#b]! | grammar[#e]!; grammar[#A] = grammar[#a]! | grammar[#B]!; grammar[#S] = grammar[#A]! & grammar[#B]! & grammar[#c]! & grammar[#d]!; return grammar; } // The canonical grammar to exercise first- and follow-set calculation, // likely originally from the dragon-book. Map createDragon() { final grammar = >{ for (final symbol in [#E, #Ep, #T, #Tp, #F]) symbol: undefined(), }; grammar[#E]!.set(grammar[#T]! & grammar[#Ep]!); grammar[#Ep]!.set((char('+') & grammar[#T]! & grammar[#Ep]!).optional()); grammar[#T]!.set(grammar[#F]! & grammar[#Tp]!); grammar[#Tp]!.set((char('*') & grammar[#F]! & grammar[#Tp]!).optional()); grammar[#F]!.set((char('(') & grammar[#E]! & char(')')) | char('i')); return grammar; } // A highly ambiguous grammar by Saichaitanya Jampana. Exploring the problem of // ambiguity in context-free grammars. Map createAmbiguous() { final grammar = >{ for (final symbol in [#S, #A, #a, #B, #b]) symbol: undefined(), }; grammar[#S]!.set((grammar[#A]! & grammar[#B]!) | grammar[#a]!); grammar[#A]!.set((grammar[#S]! & grammar[#B]!) | grammar[#b]!); grammar[#a]!.set(char('a')); grammar[#B]!.set((grammar[#B]! & grammar[#A]!) | grammar[#a]!); grammar[#b]!.set(char('b')); return grammar; } // A highly recursive parser. Map createRecursive() { final grammar = >{ for (final symbol in [#S, #P, #p, #+]) symbol: undefined(), }; grammar[#S]!.set(grammar[#P]! | grammar[#p]!); grammar[#P]!.set(grammar[#S]! & grammar[#+]! & grammar[#S]!); grammar[#p]!.set(char('p')); grammar[#+]!.set(char('+')); return grammar; } // A parser that references itself. Parser createSelfReference() { final parser = undefined(); parser.set(parser); return parser; } void expectTerminals(Iterable parsers, Iterable inputs) { final expectedInputs = {...inputs}; final actualInputs = { for (final parser in [for (final parser in parsers) parser.end()]) for (final character in [ for (var code = 32; code <= 126; code++) String.fromCharCode(code), '', ]) if (parser.accept(character)) character, }; expect(actualInputs, expectedInputs); } class PluggableLinterRule extends LinterRule { const PluggableLinterRule(super.type, super.title, this._run); final void Function(LinterRule rule, Analyzer, Parser, LinterCallback) _run; @override void run(Analyzer analyzer, Parser parser, LinterCallback callback) => _run(this, analyzer, parser, callback); } class PluggableOptimizeRule extends OptimizeRule { const PluggableOptimizeRule(this._run); final void Function( OptimizeRule rule, Analyzer analyzer, Parser parser, ReplaceParser replace, ) _run; @override void run(Analyzer analyzer, Parser parser, ReplaceParser replace) => _run(this, analyzer, parser, replace); } void main() { group('analyzer', () { test('root', () { final parser = char('a').plus(); final analyzer = Analyzer(parser); expect(analyzer.root, parser); }); test('parsers', () { final parser = char('a').plus(); final analyzer = Analyzer(parser); expect(analyzer.parsers, {parser, parser.children.first}); }); group('allChildren', () { test('single', () { final inner = char('a'); final parser = inner.plus(); final analyzer = Analyzer(parser); expect(analyzer.allChildren(parser), {inner}); expect(analyzer.allChildren(inner), isEmpty); }); test('multiple', () { final inner1 = char('a'); final inner2 = char('b'); final parser = inner1 & inner2; final analyzer = Analyzer(parser); expect(analyzer.allChildren(parser), {inner1, inner2}); expect(analyzer.allChildren(inner1), isEmpty); expect(analyzer.allChildren(inner2), isEmpty); }); test('repeated', () { final inner1 = char('a'); final inner2 = char('b'); final parser = inner1 | inner2 | inner2; final analyzer = Analyzer(parser); expect(analyzer.allChildren(parser), {inner1, inner2}); expect(analyzer.allChildren(inner1), isEmpty); expect(analyzer.allChildren(inner2), isEmpty); }); test('recursive', () { final inner1 = char('a'); final inner2 = undefined(); final parser = [inner1, inner2].toChoiceParser(); inner2.set(parser); final analyzer = Analyzer(parser); expect(analyzer.allChildren(parser), {inner1, inner2, parser}); expect(analyzer.allChildren(inner1), isEmpty); expect(analyzer.allChildren(inner2), {inner1, inner2, parser}); }); test('übersetzerbau grammar', () { final parsers = createUebersetzerbau(); final analyzer = Analyzer(parsers[#S]!); expect(analyzer.allChildren(parsers[#S]!), { parsers[#A], parsers[#B], parsers[#a], parsers[#b], parsers[#c], parsers[#d], parsers[#e], }); expect(analyzer.allChildren(parsers[#A]!), { parsers[#B], parsers[#a], parsers[#b], parsers[#e], }); expect(analyzer.allChildren(parsers[#B]!), {parsers[#b], parsers[#e]}); expect(analyzer.allChildren(parsers[#a]!), isEmpty); expect(analyzer.allChildren(parsers[#b]!), isEmpty); expect(analyzer.allChildren(parsers[#c]!), isEmpty); expect(analyzer.allChildren(parsers[#d]!), isEmpty); expect(analyzer.allChildren(parsers[#e]!), isEmpty); }); test('recursive grammar', () { final parsers = createRecursive(); final analyzer = Analyzer(parsers[#S]!); expect(analyzer.allChildren(parsers[#S]!), analyzer.parsers); expect(analyzer.allChildren(parsers[#P]!), analyzer.parsers); expect(analyzer.allChildren(parsers[#p]!), { parsers[#p]!.children.first, }); expect(analyzer.allChildren(parsers[#+]!), { parsers[#+]!.children.first, }); }); test('self reference', () { final parser = createSelfReference(); final analyzer = Analyzer(parser); expect(analyzer.allChildren(parser), {parser}); }); }); group('findPath', () { test('simple', () { final parser = char('a'); final analyzer = Analyzer(parser); final path = analyzer.findPathTo(parser, parser)!; expect(path.source, parser); expect(path.target, parser); expect(path.parsers, [parser]); expect(path.indexes, isEmpty); final paths = analyzer.findAllPathsTo(parser, parser).toList(); expect(paths, hasLength(1)); expect(paths[0].parsers, [parser]); expect(paths[0].indexes, isEmpty); }); test('choice', () { final terminal = char('a'); final parser = terminal | terminal; final analyzer = Analyzer(parser); final path = analyzer.findPathTo(parser, terminal)!; expect(path.source, parser); expect(path.target, terminal); expect(path.parsers, [parser, terminal]); expect(path.indexes, [0]); final paths = analyzer.findAllPathsTo(parser, terminal).toList(); expect(paths, hasLength(2)); expect(paths[0].parsers, [parser, terminal]); expect(paths[0].indexes, [0]); expect(paths[1].parsers, [parser, terminal]); expect(paths[1].indexes, [1]); }); test('length', () { final terminal = char('a'); final repeated = terminal.star(); final parser = repeated | terminal; final analyzer = Analyzer(parser); final path = analyzer.findPathTo(parser, terminal)!; expect(path.source, parser); expect(path.target, terminal); expect(path.parsers, [parser, terminal]); expect(path.indexes, [1]); final paths = analyzer.findAllPathsTo(parser, terminal).toList(); expect(paths, hasLength(2)); expect(paths[0].parsers, [parser, repeated, terminal]); expect(paths[0].indexes, [0, 0]); expect(paths[1].parsers, [parser, terminal]); expect(paths[1].indexes, [1]); }); test('recursive grammar', () { final parsers = createRecursive(); final analyzer = Analyzer(parsers[#S]!); expect( analyzer.findAllPaths(analyzer.root, (target) => false), isEmpty, ); }); test('self reference', () { final parser = createSelfReference(); final analyzer = Analyzer(parser); expect( analyzer.findAllPaths(analyzer.root, (target) => false), isEmpty, ); }); }); group('isNullable', () { test('plus', () { final parser = char('a').plus(); final analyzer = Analyzer(parser); expect(analyzer.isNullable(parser), isFalse); }); test('star', () { final parser = char('a').star(); final analyzer = Analyzer(parser); expect(analyzer.isNullable(parser), isTrue); }); test('optional', () { final parser = char('a').optional(); final analyzer = Analyzer(parser); expect(analyzer.isNullable(parser), isTrue); }); test('choice', () { final parser = char('a').or(char('b')); final analyzer = Analyzer(parser); expect(analyzer.isNullable(parser), isFalse); }); test('epsilon choice', () { final parser = char('a').or(epsilon()); final analyzer = Analyzer(parser); expect(analyzer.isNullable(parser), isTrue); }); test('sequence', () { final parser = char('a').seq(char('b')); final analyzer = Analyzer(parser); expect(analyzer.isNullable(parser), isFalse); }); test('epsilon sequence', () { final parser = epsilon().seq(char('a')); final analyzer = Analyzer(parser); expect(analyzer.isNullable(parser), isFalse); }); test('optional sequence', () { final parser = char('a').optional().seq(char('b')); final analyzer = Analyzer(parser); expect(analyzer.isNullable(parser), isFalse); }); test('übersetzerbau grammar', () { final parsers = createUebersetzerbau(); final analyzer = Analyzer(parsers[#S]!); expect(analyzer.isNullable(parsers[#S]!), isFalse); expect(analyzer.isNullable(parsers[#A]!), isTrue); expect(analyzer.isNullable(parsers[#B]!), isTrue); expect(analyzer.isNullable(parsers[#a]!), isFalse); expect(analyzer.isNullable(parsers[#b]!), isFalse); expect(analyzer.isNullable(parsers[#c]!), isFalse); expect(analyzer.isNullable(parsers[#d]!), isFalse); expect(analyzer.isNullable(parsers[#e]!), isTrue); }); test('dragon grammar', () { final parsers = createDragon(); final analyzer = Analyzer(parsers[#E]!); expect(analyzer.isNullable(parsers[#E]!), isFalse); expect(analyzer.isNullable(parsers[#Ep]!), isTrue); expect(analyzer.isNullable(parsers[#T]!), isFalse); expect(analyzer.isNullable(parsers[#Tp]!), isTrue); expect(analyzer.isNullable(parsers[#F]!), isFalse); }); test('ambiguous grammar', () { final parsers = createAmbiguous(); final analyzer = Analyzer(parsers[#S]!); expect(analyzer.isNullable(parsers[#S]!), isFalse); expect(analyzer.isNullable(parsers[#A]!), isFalse); expect(analyzer.isNullable(parsers[#B]!), isFalse); expect(analyzer.isNullable(parsers[#a]!), isFalse); expect(analyzer.isNullable(parsers[#b]!), isFalse); }); test('recursive grammar', () { final parsers = createRecursive(); final analyzer = Analyzer(parsers[#S]!); expect(analyzer.isNullable(parsers[#S]!), isFalse); expect(analyzer.isNullable(parsers[#P]!), isFalse); expect(analyzer.isNullable(parsers[#p]!), isFalse); }); test('self reference', () { final parser = createSelfReference(); final analyzer = Analyzer(parser); expect(analyzer.isNullable(parser), isFalse); }); }); group('first-set', () { test('plus', () { final parser = char('a').plus(); final analyzer = Analyzer(parser); expectTerminals(analyzer.firstSet(parser), ['a']); }); test('star', () { final parser = char('a').star(); final analyzer = Analyzer(parser); expectTerminals(analyzer.firstSet(parser), ['a', '']); }); test('optional', () { final parser = char('a').optional(); final analyzer = Analyzer(parser); expectTerminals(analyzer.firstSet(parser), ['a', '']); }); test('choice', () { final parser = char('a').or(char('b')); final analyzer = Analyzer(parser); expectTerminals(analyzer.firstSet(parser), ['a', 'b']); }); test('epsilon choice', () { final parser = char('a').or(epsilon()); final analyzer = Analyzer(parser); expectTerminals(analyzer.firstSet(parser), ['a', '']); }); test('sequence', () { final parser = char('a').seq(char('b')); final analyzer = Analyzer(parser); expectTerminals(analyzer.firstSet(parser), ['a']); }); test('epsilon sequence', () { final parser = epsilon().seq(char('a')); final analyzer = Analyzer(parser); expectTerminals(analyzer.firstSet(parser), ['a']); }); test('optional sequence', () { final parser = char('a').optional().seq(char('b')); final analyzer = Analyzer(parser); expectTerminals(analyzer.firstSet(parser), ['a', 'b']); }); test('übersetzerbau grammar', () { final parsers = createUebersetzerbau(); final analyzer = Analyzer(parsers[#S]!); expectTerminals(analyzer.firstSet(parsers[#S]!), ['a', 'b', 'c']); expectTerminals(analyzer.firstSet(parsers[#A]!), ['a', 'b', '']); expectTerminals(analyzer.firstSet(parsers[#B]!), ['b', '']); expectTerminals(analyzer.firstSet(parsers[#a]!), ['a']); expectTerminals(analyzer.firstSet(parsers[#b]!), ['b']); expectTerminals(analyzer.firstSet(parsers[#c]!), ['c']); expectTerminals(analyzer.firstSet(parsers[#d]!), ['d']); expectTerminals(analyzer.firstSet(parsers[#e]!), ['']); }); test('dragon grammar', () { final parsers = createDragon(); final analyzer = Analyzer(parsers[#E]!); expectTerminals(analyzer.firstSet(parsers[#E]!), ['(', 'i']); expectTerminals(analyzer.firstSet(parsers[#Ep]!), ['+', '']); expectTerminals(analyzer.firstSet(parsers[#T]!), ['(', 'i']); expectTerminals(analyzer.firstSet(parsers[#Tp]!), ['*', '']); expectTerminals(analyzer.firstSet(parsers[#F]!), ['(', 'i']); }); test('ambiguous grammar', () { final parsers = createAmbiguous(); final analyzer = Analyzer(parsers[#S]!); expectTerminals(analyzer.firstSet(parsers[#S]!), ['a', 'b']); expectTerminals(analyzer.firstSet(parsers[#A]!), ['a', 'b']); expectTerminals(analyzer.firstSet(parsers[#B]!), ['a']); expectTerminals(analyzer.firstSet(parsers[#a]!), ['a']); expectTerminals(analyzer.firstSet(parsers[#b]!), ['b']); }); test('recursive grammar', () { final parsers = createRecursive(); final analyzer = Analyzer(parsers[#S]!); expectTerminals(analyzer.firstSet(parsers[#S]!), ['p']); expectTerminals(analyzer.firstSet(parsers[#P]!), ['p']); expectTerminals(analyzer.firstSet(parsers[#p]!), ['p']); }); test('self reference', () { final parser = createSelfReference(); final analyzer = Analyzer(parser); expectTerminals(analyzer.firstSet(parser), []); }); }); group('follow-set', () { test('plus', () { final parser = char('a').plus(); final analyzer = Analyzer(parser); expectTerminals(analyzer.followSet(parser), ['']); expectTerminals(analyzer.followSet(parser.children[0]), ['a', '']); }); test('star', () { final parser = char('a').star(); final analyzer = Analyzer(parser); expectTerminals(analyzer.followSet(parser), ['']); expectTerminals(analyzer.followSet(parser.children[0]), ['a', '']); }); test('optional', () { final parser = char('a').optional(); final analyzer = Analyzer(parser); expectTerminals(analyzer.followSet(parser), ['']); expectTerminals(analyzer.followSet(parser.children[0]), ['']); }); test('choice', () { final parser = char('a').or(char('b')); final analyzer = Analyzer(parser); expectTerminals(analyzer.followSet(parser), ['']); expectTerminals(analyzer.followSet(parser.children[0]), ['']); expectTerminals(analyzer.followSet(parser.children[1]), ['']); }); test('epsilon choice', () { final parser = char('a').or(epsilon()); final analyzer = Analyzer(parser); expectTerminals(analyzer.followSet(parser), ['']); expectTerminals(analyzer.followSet(parser.children[0]), ['']); expectTerminals(analyzer.followSet(parser.children[1]), ['']); }); test('sequence', () { final parser = char('a').seq(char('b')); final analyzer = Analyzer(parser); expectTerminals(analyzer.followSet(parser), ['']); expectTerminals(analyzer.followSet(parser.children[0]), ['b']); expectTerminals(analyzer.followSet(parser.children[1]), ['']); }); test('epsilon sequence', () { final parser = epsilon().seq(char('a')); final analyzer = Analyzer(parser); expectTerminals(analyzer.followSet(parser), ['']); expectTerminals(analyzer.followSet(parser.children[0]), ['a']); expectTerminals(analyzer.followSet(parser.children[1]), ['']); }); test('optional sequence', () { final parser = char('a').seq(char('b').optional()); final analyzer = Analyzer(parser); expectTerminals(analyzer.followSet(parser), ['']); expectTerminals(analyzer.followSet(parser.children[0]), ['b', '']); expectTerminals(analyzer.followSet(parser.children[1]), ['']); }); test('übersetzerbau grammar', () { final parsers = createUebersetzerbau(); final analyzer = Analyzer(parsers[#S]!); expectTerminals(analyzer.followSet(parsers[#S]!), ['']); expectTerminals(analyzer.followSet(parsers[#A]!), ['b', 'c']); expectTerminals(analyzer.followSet(parsers[#B]!), ['b', 'c']); expectTerminals(analyzer.followSet(parsers[#a]!), ['b', 'c']); expectTerminals(analyzer.followSet(parsers[#b]!), ['b', 'c']); expectTerminals(analyzer.followSet(parsers[#c]!), ['d']); expectTerminals(analyzer.followSet(parsers[#d]!), ['']); expectTerminals(analyzer.followSet(parsers[#e]!), ['b', 'c']); }); test('dragon grammar', () { final parsers = createDragon(); final analyzer = Analyzer(parsers[#E]!); expectTerminals(analyzer.followSet(parsers[#E]!), [')', '']); expectTerminals(analyzer.followSet(parsers[#Ep]!), [')', '']); expectTerminals(analyzer.followSet(parsers[#T]!), [')', '+', '']); expectTerminals(analyzer.followSet(parsers[#Tp]!), [')', '+', '']); expectTerminals(analyzer.followSet(parsers[#F]!), [')', '+', '*', '']); }); test('ambiguous grammar', () { final parsers = createAmbiguous(); final analyzer = Analyzer(parsers[#S]!); expectTerminals(analyzer.followSet(parsers[#S]!), ['a', '']); expectTerminals(analyzer.followSet(parsers[#A]!), ['a', 'b', '']); expectTerminals(analyzer.followSet(parsers[#B]!), ['a', 'b', '']); expectTerminals(analyzer.followSet(parsers[#a]!), ['a', 'b', '']); expectTerminals(analyzer.followSet(parsers[#b]!), ['a', 'b', '']); }); test('recursive grammar', () { final parsers = createRecursive(); final analyzer = Analyzer(parsers[#S]!); expectTerminals(analyzer.followSet(parsers[#S]!), ['+', '']); expectTerminals(analyzer.followSet(parsers[#P]!), ['+', '']); expectTerminals(analyzer.followSet(parsers[#p]!), ['+', '']); }); test('self reference', () { final parser = createSelfReference(); final analyzer = Analyzer(parser); expectTerminals(analyzer.followSet(parser), ['']); }); }); group('cycle-set', () { test('übersetzerbau grammar', () { final parsers = createUebersetzerbau(); final analyzer = Analyzer(parsers[#S]!); for (final parser in parsers.values) { expect(analyzer.cycleSet(parser), isEmpty); } }); test('dragon grammar', () { final parsers = createDragon(); final analyzer = Analyzer(parsers[#E]!); for (final parser in parsers.values) { expect(analyzer.cycleSet(parser), isEmpty); } }); test('ambiguous grammar', () { final parsers = createAmbiguous(); final analyzer = Analyzer(parsers[#S]!); expect( analyzer.cycleSet(parsers[#S]!), allOf(hasLength(6), containsAll([parsers[#S], parsers[#A]])), ); expect( analyzer.cycleSet(parsers[#A]!), allOf(hasLength(6), containsAll([parsers[#S], parsers[#A]])), ); expect( analyzer.cycleSet(parsers[#B]!), allOf(hasLength(3), containsAll([parsers[#B]])), ); expect(analyzer.cycleSet(parsers[#a]!), isEmpty); expect(analyzer.cycleSet(parsers[#b]!), isEmpty); }); test('recursive grammar', () { final parsers = createRecursive(); final analyzer = Analyzer(parsers[#S]!); expect( analyzer.cycleSet(parsers[#S]!), allOf(hasLength(4), containsAll([parsers[#S], parsers[#P]])), ); expect( analyzer.cycleSet(parsers[#P]!), allOf(hasLength(4), containsAll([parsers[#S], parsers[#P]])), ); expect(analyzer.cycleSet(parsers[#p]!), isEmpty); }); test('self reference', () { final parser = createSelfReference(); final analyzer = Analyzer(parser); expect( analyzer.cycleSet(parser), allOf(hasLength(1), containsAll([parser])), ); }); }); }); group('iterable', () { test('single', () { final parser1 = lowercase(); final parsers = allParser(parser1).toList(); expect(parsers, [parser1]); }); test('nested', () { final parser3 = lowercase(); final parser2 = parser3.star(); final parser1 = parser2.flatten(); final parsers = allParser(parser1).toList(); expect(parsers, [parser1, parser2, parser3]); }); test('branched', () { final parser3 = lowercase(); final parser2 = uppercase(); final parser1 = parser2.seq(parser3); final parsers = allParser(parser1).toList(); expect(parsers, [parser1, parser2, parser3]); }); test('duplicated', () { final parser2 = uppercase(); final parser1 = parser2.seq(parser2); final parsers = allParser(parser1).toList(); expect(parsers, [parser1, parser2]); }); test('knot', () { final parser1 = undefined(); parser1.set(parser1); final parsers = allParser(parser1).toList(); expect(parsers, [parser1]); }); test('looping', () { final parser1 = undefined(); final parser2 = undefined(); final parser3 = undefined(); parser1.set(parser2); parser2.set(parser3); parser3.set(parser1); final parsers = allParser(parser1).toList(); expect(parsers, [parser1, parser2, parser3]); }); }); group('linter', () { test('rules called on all parsers', () { final seen = {}; final input = char('a') | char('b'); final rule = PluggableLinterRule( LinterType.error, 'Fake Rule', (rule, analyzer, parser, callback) => seen.add(parser), ); final results = linter( input, rules: [rule], callback: (issue) => fail('Unexpected callback'), ); expect(results, isEmpty); expect(seen, {input, input.children[0], input.children[1]}); }); test('issue triggered', () { final input = 'trigger'.toParser(); final called = []; final rule = PluggableLinterRule(LinterType.error, 'Fake Rule', ( rule, analyzer, parser, callback, ) { expect(parser, same(input)); callback(LinterIssue(rule, parser, 'Described')); }); expect( rule, isLinterRule( type: LinterType.error, title: 'Fake Rule', toString: isToString( name: 'LinterRule', rest: ['(type: LinterType.error, title: Fake Rule)'], ), ), ); final results = linter(input, rules: [rule], callback: called.add); expect(results, [ isLinterIssue( rule: same(rule), type: LinterType.error, title: 'Fake Rule', parser: same(input), description: 'Described', toString: isToString( name: 'LinterIssue', rest: [ '(type: LinterType.error, title: Fake Rule', 'description: Described)', ], ), ), ]); expect(called, results); }); group('rules', () { group('character repetition', () { const rules = [linter_rules.CharacterRepeater()]; test('with character predicate parser', () { final parser = char('a').star().flatten(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.warning, title: 'Character repeater', ), ]); }); test('with any parser', () { final parser = any().plus().flatten(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.warning, title: 'Character repeater', ), ]); }); test('without issue', () { final parser = char('a').plus().token(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('left recursion', () { const rules = [linter_rules.LeftRecursion()]; test('with issue', () { final parser = createSelfReference(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser.children[0], type: LinterType.error, title: 'Left recursion', ), ]); }); test('without issue', () { final parser = digit(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('nested choice', () { const rules = [linter_rules.NestedChoice()]; test('with issue', () { final parser = [ char('1'), [char('2'), char('3')].toChoiceParser(), char('4'), ].toChoiceParser(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.info, title: 'Nested choice', ), ]); }); test('without issue', () { final parser = [ char('1'), [char('2'), char('3')].toChoiceParser().flatten(), char('4'), ].toChoiceParser(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('nullable repeater', () { const rules = [linter_rules.NullableRepeater()]; test('with issue', () { final parser = epsilon().star().optional(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser.children[0], type: LinterType.error, title: 'Nullable repeater', ), ]); }); test('without issue', () { final parser = digit().star().optional(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('overlapping choice', () { const rules = [linter_rules.OverlappingChoice()]; test('with issue', () { final parser = [ char('1'), char('2') & char('a'), char('2') & char('b'), char('3'), ].toChoiceParser(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.info, title: 'Overlapping choice', ), ]); }); test('without issue', () { final parser = [char('1'), char('2'), char('3')].toChoiceParser(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('repeated choice', () { const rules = [linter_rules.RepeatedChoice()]; test('with issue', () { final parser = [ char('1'), char('2'), char('3'), char('2'), char('4'), ].toChoiceParser(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.warning, title: 'Repeated choice', ), ]); }); test('without issue', () { final parser = [ char('1'), char('2'), char('3'), char('4'), ].toChoiceParser(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('unnecessary flatten', () { const rules = [linter_rules.UnnecessaryFlatten()]; test('with issue', () { final parser = any().flatten(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.warning, title: 'Unnecessary flatten', ), ]); }); test('without issue', () { final parser = any().optional().flatten(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('unnecessary resolvable', () { const rules = [linter_rules.UnnecessaryResolvable()]; test('with issue', () { final parser = char('a').settable(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.warning, title: 'Unnecessary resolvable', ), ]); }); test('without issue', () { final parser = char('a'); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('unoptimized flatten', () { const rules = [linter_rules.UnoptimizedFlatten()]; test('with issue', () { final parser = any().flatten(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.info, title: 'Unoptimized flatten', ), ]); }); test('without issue', () { final parser = any().flatten(message: 'anything really'); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('unreachable choice', () { const rules = [linter_rules.UnreachableChoice()]; test('with issue', () { final parser = [ char('1'), char('2'), epsilon(), char('3'), ].toChoiceParser(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.warning, title: 'Unreachable choice', ), ]); }); test('without issue', () { final parser = [char('1'), char('2'), char('3')].toChoiceParser(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('unresolved settable', () { const rules = [linter_rules.UnresolvedSettable()]; test('with issue', () { final parser = undefined(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.error, title: 'Unresolved settable', ), ]); }); test('without issue', () { final parser = digit().settable(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); group('unused result', () { const rules = [linter_rules.UnusedResult()]; test('with issue', () { final parser = digit().map(int.parse).star().flatten(); final results = linter(parser, rules: rules); expect(results, [ isLinterIssue( parser: parser, type: LinterType.info, title: 'Unused result', ), ]); }); test('without issue', () { final parser = digit().star().flatten(); final results = linter(parser, rules: rules); expect(results, isEmpty); }); }); }); group('regressions', () { test('separatedBy and nullable repeater', () { const rules = [linter_rules.NullableRepeater()]; // Both repeater and separator are nullable, this might cause an // infinite loop. expect(linter(epsilon().starSeparated(epsilon()), rules: rules), [ isLinterIssue(title: 'Nullable repeater'), ]); // If either the repeater or the separator is non-nullable, everything // is fine. expect(linter(epsilon().starSeparated(any()), rules: rules), isEmpty); expect(linter(any().starSeparated(epsilon()), rules: rules), isEmpty); }); }); }); group('transform', () { test('copy', () { final input = lowercase().settable(); final output = transformParser(input, (parser) => parser); expect(input, isNot(output)); expect(input.isEqualTo(output), isTrue); expect(input.children.single, isNot(output.children.single)); }); test('root', () { final source = lowercase(); final input = source; final target = uppercase(); final output = transformParser( input, (parser) => source.isEqualTo(parser) ? target as Parser : parser, ); expect(input, isNot(output)); expect(input.isEqualTo(output), isFalse); expect(input, source); expect(output, target); }); test('single', () { final source = lowercase(); final input = source.settable(); final target = uppercase(); final output = transformParser( input, (parser) => source.isEqualTo(parser) ? target as Parser : parser, ); expect(input, isNot(output)); expect(input.isEqualTo(output), isFalse); expect(input.children.single, source); expect(output.children.single, target); }); test('double', () { final source = lowercase(); final input = source & source; final target = uppercase(); final output = transformParser( input, (parser) => source.isEqualTo(parser) ? target as Parser : parser, ); expect(input, isNot(output)); expect(input.isEqualTo(output), isFalse); expect(input.isEqualTo(source & source), isTrue); expect(input.children.first, input.children.last); expect(output.isEqualTo(target & target), isTrue); expect(output.children.first, output.children.last); }); test('loop (existing)', () { final inner = failure().settable(); final outer = inner.settable().settable(); inner.set(outer); final output = transformParser(outer, (parser) => parser); expect(outer, isNot(output)); expect(outer.isEqualTo(output), isTrue); final inputs = allParser(outer).toSet(); final outputs = allParser(output).toSet(); for (final input in inputs) { expect(outputs.contains(input), isFalse); } for (final output in outputs) { expect(inputs.contains(output), isFalse); } }); test('loop (new)', () { final source = lowercase(); final input = source; final inner = failure().settable(); final outer = inner.settable().settable(); inner.set(outer); final output = transformParser( input, (parser) => source.isEqualTo(parser) ? outer as Parser : parser, ); expect(input, isNot(output)); expect(input.isEqualTo(output), isFalse); expect(output.isEqualTo(outer), isTrue); }); }); group('optimize', () { test('rules called on all parsers', () { final seen = {}; final input = char('a') | char('b'); final rule = PluggableOptimizeRule( (rule, analyzer, parser, replace) => seen.add(parser), ); final result = optimize( input, rules: [rule], callback: (source, target) => fail('No callback expected'), ); expect(result, same(input)); expect(seen, {input, input.children[0], input.children[1]}); }); test('root replacement performed', () { final input = 'input'.toParser(), output = 'output'.toParser(); final rule = PluggableOptimizeRule((rule, analyzer, parser, replace) { expect(parser, same(input)); replace(input as Parser, output as Parser); }); final result = optimize( input, rules: [rule], callback: (source, target) { expect(source, input); expect(target, output); }, ); expect(result, same(output)); }); test('child replacement performed', () { final input = char('a') | char('b'), replacement = char('c'); final rule = PluggableOptimizeRule((rule, analyzer, parser, replace) { if (parser is CharacterParser && (parser as CharacterParser).message == '"b" expected') { replace(parser, replacement as Parser); } }); final result = optimize( input, rules: [rule], callback: (source, target) { expect(source, input.children[1]); expect(target, replacement); }, ); expect(result, same(input)); expect(result.children[1], same(replacement)); }); group('rules', () { group('character repeater', () { const rules = [optimize_rules.CharacterRepeater()]; test('with predicate parser', () { final character = char('a'); final parser = character.repeat(2, 3).flatten(); final result = optimize(parser, rules: rules); expect( result, isA() .having((p) => p.min, 'min', 2) .having((p) => p.max, 'max', 3) .having((p) => p.message, 'message', '"a" expected'), ); }); test('with any parser', () { final character = any(); final parser = character.repeat(3, 5).flatten(); final result = optimize(parser, rules: rules); expect( result, isA() .having((p) => p.min, 'min', 3) .having((p) => p.max, 'max', 5) .having((p) => p.message, 'message', 'input expected'), ); }); test('without optimization', () { final parser = char('a').plus().token(); final result = optimize(parser, rules: rules); expect(result, same(parser)); }); }); group('nested choice', () { const rules = [optimize_rules.FlattenChoice()]; test('with issue', () { final parser = [ char('1'), [ char('2'), char('3'), ].toChoiceParser(failureJoiner: selectFarthest), char('4'), ].toChoiceParser(failureJoiner: selectFarthest); final result = optimize(parser, rules: rules); expect( result, isA>() .having( (p) => p.children, 'children', containsAllInOrder([ parser.children[0], parser.children[1].children[0], parser.children[1].children[1], parser.children[2], ]), ) .having( (p) => p.failureJoiner, 'failureJoiner', selectFarthest, ), ); }); test('without optimization (no nesting)', () { final parser = [char('1'), char('2'), char('3')].toChoiceParser(); final result = optimize( parser, rules: rules, callback: (source, target) => fail('No replacement expected'), ); expect(result, same(parser)); }); test('without optimization (different joiner)', () { final parser = [ char('1'), [ char('2'), char('3'), ].toChoiceParser(failureJoiner: selectFarthest), char('4'), ].toChoiceParser(); final result = optimize( parser, rules: rules, callback: (source, target) => fail('No replacement expected'), ); expect(result, same(parser)); }); }); group('remove delegate', () { const rules = [optimize_rules.RemoveDelegate()]; test('with single settable', () { final parser = char('a').settable(); final result = optimize(parser, rules: rules); expect(result, same(parser.children[0])); }); test('with single label', () { final parser = char('a').labeled('hello'); final result = optimize(parser, rules: rules); expect(result, same(parser.children[0])); }); test('with repeated settable', () { final parser = char('a').settable().settable(); final result = optimize(parser, rules: rules); expect(result, same(parser.children[0])); }); test('with loop', () { final parser = undefined(); parser.set(parser); final result = optimize(parser, rules: rules); expect(result, same(parser)); }); }); group('remove duplicate', () { const rules = [optimize_rules.RemoveDuplicate()]; test('with duplicate', () { final parser = lowercase() & lowercase(); final result = optimize(parser, rules: rules); expect(result.children.first, same(result.children.last)); }); test('without duplicate', () { final parser = lowercase() & lowercase(message: 'lower'); final result = optimize( parser, rules: rules, callback: (source, target) => fail('No replacement expected'), ); expect(result.children.first, isNot(same(result.children.last))); }); }); }); }); }