diff --git a/.github/dependabot.yml b/.github/dependabot.yml index daec318..c9b2105 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,10 @@ version: 2 updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" - package-ecosystem: "maven" directory: "/" schedule: - interval: "daily" + interval: "weekly" diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index ead191d..ae6d354 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,4 +1,5 @@ name: Java CI + on: push: branches: [ main ] @@ -6,19 +7,25 @@ on: branches: [ main ] schedule: - cron: "0 0 * * 0" + workflow_dispatch: +defaults: + run: + shell: bash +permissions: + contents: read jobs: build: runs-on: ubuntu-latest strategy: matrix: - java: [ '11', '11.0.3', '14', '14.0.1' ] - name: Java ${{ matrix.Java }} + java: [ '21' ] + name: Java ${{ matrix.java }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 with: distribution: 'zulu' - java-version: ${{ matrix.Java }} + java-version: ${{ matrix.java }} - name: Build with Maven run: mvn -B package --file pom.xml diff --git a/.gitignore b/.gitignore index 1d30442..887747e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ bazel-genfiles bazel-out bazel-petitparser bazel-testlogs -target/ \ No newline at end of file +target/ +.vscode/ \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..ffcab66 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1cacabe --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,180 @@ +# Development Charter & Agent Guidelines + +## General Instructions + +- Deliver code, tests, and documentation directly with zero introductory fluff or pleasantries. +- Strictly adhere to 2 spaces for indentation. +- Preserve zero external runtime dependencies for `petitparser-core` (rely exclusively on the Java standard library). +- Target Java 21 bytecode compatibility (`21`). + +--- + +## Backward Compatibility & Java Idioms + +### Backward Compatibility Principles + +- **No Breaking Changes**: Existing public methods, constructors, and classes must remain intact. Downstream modules (`petitparser-json`, `petitparser-xml`, `petitparser-smalltalk`) and user code must continue to compile and pass tests without modification. +- **Additive Evolution**: Introduce new functionality alongside existing APIs (e.g., provide `starSeparated` returning `SeparatedList` without removing `separatedBy` returning `List`). +- **Raw-Type Compatibility**: When introducing generic parameters (`Parser`, `Result`, etc.), ensure raw-type usage compiles cleanly without requiring modifications to existing untyped grammars. + +### Java-Centric Best Practices + +- **Idiomatic Java vs. Verbatim Dart**: Avoid mechanical translations of Dart-specific features (such as extension methods, positional record tuples, or operator overloading). Leverage standard Java paradigms: + - Fluent method chaining on `Parser` and builders. + - Standard functional interfaces (`java.util.function.Function`, `Predicate`, `BiFunction`, `Supplier`, `Consumer`). + - Java `Stream` and lazy `Iterable`/`Iterator` for streaming matching and graph traversals. + - Immutability for core state (`Context`, `Result`, `Token`, `SeparatedList`, `Tuple`) using defensive copies and unmodifiable collections. + - Null-safety checks using `Objects.requireNonNull(arg, "message")`. + +### Type System & Generics Guidelines + +- **Dual-Nature Tuples**: Implement `Tuple2` through `Tuple9` extending `AbstractList`. + - Enables strongly-typed accessors (`tuple.first()`, `tuple.second()`, etc.) without losing compatibility with APIs expecting `List`. + - Seamlessly passes equality against standard lists (`tuple.equals(Arrays.asList(...)) == true`). +- **Direct-Mapped Sequences**: Provide fluent `.then()` sequence chaining returning `SequenceParser2` through `SequenceParser9`, offering direct multi-argument mapping (`.map((a, b) -> ...)` via `BiFunction`, `Function3`, etc.) so users rarely need to inspect the underlying `Tuple`. +- **PECS (Producer Extends, Consumer Super)**: + - Input callbacks consume: `Function`, `Predicate`, `BiFunction`. + - Child parser acceptance produces: `Parser`. +- **Local Type Inference (`var`) Preservation**: + - Keep return types specific rather than introducing unnecessary wildcard returns that cause `var` to degrade to `Parser`. + - Provide `or(Parser other)` to retain exact type `R`, and `orWiden(Parser other)` for explicit widening across differing types. +- **Production Keys vs. Method References**: + - Unlike Dart, Java method references (`this::rule`) do NOT preserve object identity or equality (`this::foo != this::foo`). + - Provide type-safe `Production` keys (`Production EXPR = production("expr")`) in `GrammarDefinition` alongside `SettableParser` fields to deliver 100% type safety and IDE refactoring without synthetic lambda reflection. + +--- + +## Java Performance & Efficiency Guidelines + +### Zero-Allocation Fast Parsing + +- Subclasses must implement an optimized `int fastParseOn(String buffer, int position)`: + - Never allocate `Context`, `Result`, `Success`, or `Failure` objects on the fast path. + - Avoid boxing primitive `char` or `int` values. + - Never throw exceptions on normal parse failure; return `-1` immediately. + +### Primitive Specialization & Character Lookups + +- **$O(1)$ Character Lookup Tables**: For ASCII/Latin-1 character sets, specialize character predicates with boolean lookup tables (`boolean[256]`) or primitive bitmasks (`BitSet`/`long[]`) to perform instantaneous $O(1)$ checks instead of linear scans or branching. +- **Cache-Friendly Range Searching**: For sparse/Unicode ranges, use sorted primitive arrays (`char[] starts`, `char[] stops`) with `Arrays.binarySearch`. +- **Pre-Merged Ranges**: Automatically merge adjacent and overlapping ranges during grammar construction so runtime checks execute minimum comparisons. + +### Direct Substring Extraction (Zero-List Lexing) + +- In lexing and tokenization, eliminate intermediate `List` allocations. Parsers matching sequences of characters (`starString()`, `plusString()`, `repeatString()`, `flatten()`) must scan characters using primitive loop indexes on `buffer.charAt(i)` and extract the final sub-string with a single `buffer.substring(start, stop)` call. + +### JVM Intrinsics & String Matching + +- Leverage HotSpot-intrinsic methods such as `String.startsWith(prefix, position)` and `String.regionMatches` for multi-character literals to benefit from vectorized SIMD instructions. + +### Singleton Reuse & Allocation Minimization + +- Reuse immutable static singleton instances for stateless parsers and predicates (e.g. `PositionParser`, `EpsilonParser`, `NewlineParser`, `ConstantCharPredicate.any()`, `DigitCharPredicate.INSTANCE`). + +--- + +## Architecture & Code Quality + +### Package Structure + +Implementation files must strictly adhere to the established package layout: + +- `org.petitparser.context`: Context, Result, Success, Failure, Token, ParseError. +- `org.petitparser.parser`: Parser base class and common combinator methods. +- `org.petitparser.parser.actions`: Action, Trimming, Flatten, Where, Continuation parsers. +- `org.petitparser.parser.combinators`: Choice, Sequence, Settable, Optional, Not, And, Skip, Label, SequentialParser, ResolvableParser. +- `org.petitparser.parser.primitive`: CharacterParser, CharacterPredicate, StringParser, EpsilonParser, FailureParser, PositionParser, NewlineParser. +- `org.petitparser.parser.repeating`: Possessive, Greedy, Lazy, Limited, RepeatingCharacterParser, SeparatedRepeatingParser, SeparatedList. +- `org.petitparser.tools`: GrammarDefinition, GrammarParser, ExpressionBuilder, Indent. +- `org.petitparser.utils`: Mirror, Optimizer, Analyzer, Linter, Profiler, Tracer, FailureJoiner, Functions. + +### Parser Contracts + +Every concrete `Parser` subclass must implement: + +1. `Result parseOn(Context context)`: Primary parsing logic allocating `Success` or `Failure`. +2. `int fastParseOn(String buffer, int position)`: Optimized parsing logic without allocating context/result objects. Returns new position on success, `-1` on failure. +3. `Parser copy()`: Shallow copy of the parser. +4. `boolean hasEqualProperties(Parser other)`: State comparison if the parser adds fields/configuration. +5. `List getChildren()` and `void replace(Parser source, Parser target)`: If the parser references child/delegate parsers. +6. `String toString()`: Human-readable representation including key configuration or message. + +### Documentation & Comments + +- Provide Javadoc comments (`/** ... */`) for all public classes, interfaces, constructors, methods, and constants. +- Include `@param`, `@return`, and `@throws` tags where appropriate. +- Include executable, accurate code examples for non-trivial public combinators and builders. +- Use `{@link ...}` and `{@code ...}` tags to reference classes and code constructs. + +--- + +## Testing & Quality Assurance + +### Parallel Test Hierarchy + +- Maintain a strict 1:1 parallel package and class hierarchy between source and test trees: + - Source: `src/main/java/org/petitparser/pkg/MyClass.java` + - Test: `src/test/java/org/petitparser/pkg/MyClassTest.java` +- Existing root-level tests in `org.petitparser.*` should be preserved, while new parser tests must follow the parallel package structure. + +### Test Coverage Expectations + +- Unit tests are required for every line of code, including edge cases, fast-parse paths, error conditions, copy semantics, and structural equality (`isEqualTo`). +- Every parser must be tested with: + 1. Passing inputs verifying parsed value and position. + 2. Failing inputs verifying failure message and exact failure position. + 3. Prefix/sub-string inputs to test non-zero start positions and bounds. + 4. Fast-parse path (`accept(input)` or `fastParseOn(buffer, position)`). + 5. Copy semantics (`copy()`) and structural equality (`isEqualTo()`). + +### Running Tests + +Execute the Maven wrapper from the repository root: + +- Run all tests across the reactor: + + ```bash + ./mvnw test + ``` + +- Run tests only in `petitparser-core`: + + ```bash + ./mvnw test -pl petitparser-core + ``` + +- Run a specific test class: + + ```bash + ./mvnw test -pl petitparser-core -Dtest=MyClassTest + ``` + +- Run a specific test method: + + ```bash + ./mvnw test -pl petitparser-core -Dtest=MyClassTest#testMethodName + ``` + +- Clean and build: + + ```bash + ./mvnw clean test + ``` + +--- + +## Code Formatting & Spotless + +Execute Spotless through the Maven wrapper: + +- Apply formatting across all source and test files: + + ```bash + ./mvnw spotless:apply + ``` + +- Verify formatting without modifying files (also run automatically during `verify` and CI): + + ```bash + ./mvnw spotless:check + ``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b204999 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +## 2.5.0 + +- Upgraded baseline to Java 21 LTS (`21`) and modernized GitHub Actions CI. +- Added fluent `.then(...)` sequence chaining with strongly-typed `SequenceParser2` through `SequenceParser9`. +- Added multi-arity `.map(...)` callbacks (`BiFunction`, `Function3` through `Function9`) for type-safe sequence unpacking. +- Introduced dual-nature tuples (`Tuple2` through `Tuple9`) extending `AbstractList` with typed accessors and standard list compatibility. +- Added type-safe grammar productions with `Production` keys and `GrammarParser` typed lookup. +- Modernized `ExpressionBuilder` with full generic typing, builder-level primitive helpers, loopback access, optional operator groups, `foldLeft`/`foldRight`, and typed action callbacks. +- Added zero-list repeating character parsers (`starString()`, `plusString()`, `timesString()`, `repeatString()`) with SIMD string matching. +- Added full Unicode code point and surrogate pair parsing with `UnicodeCharacterParser`. +- Added `CharacterPredicate` AST hierarchy with range merging and automatic lookup table optimization. +- Added indentation-sensitive parsing engine (`Indent`) for indentation-aware grammars (`Indent.during`, `equal`, `greater`, etc.). +- Added grammar `Analyzer` for computing reachability, cycles, nullability, FIRST sets, and FOLLOW sets. +- Added grammar `Linter` with 13 automated rules detecting anti-patterns. +- Added grammar `Optimizer` rule engine with choice flattening, character repeater rewriting, and delegate removal. +- Added `Progress` visual execution debugger and `ProgressFrame` for inspecting parser backtracking and execution graphs. +- Added lazy stream matching (`matchesAsStream()`) and offset-based searching on `Parser`. +- Added zero-allocation fast parse (`fastParseOn`) across all parsers. +- Added `skip(...)` and delimiter combinators, `where(...)` filtering combinator, `label(...)` parser, and `position()` / `newline()` primitives. +- Comprehensive documentation rewrite and examples modernization in `README.md` and `ExamplesTest.java`. +- Near 100% test coverage across all reactor modules. + +## 2.4.0 + +- Added Java Platform Module System (JPMS) support with `module-info.java`. +- Added configurable failure join strategy on `ChoiceParser` (`FailureJoiner`). +- Improved character parser failure messages and XML grammar performance. +- Migrated continuous integration pipelines to GitHub Actions. + +## 2.3.0 + +- Introduced zero-allocation fast parse mode (`fastParseOn`). +- Added parenthesis expression support to `ExpressionBuilder`. +- Optimized XML and JSON grammars to take advantage of fast parsing. +- Improved Java 11 (LTS) compatibility. + +## 2.2.0 + +- Preserved parser generic types during `copy()`. +- Cleaned up build configuration for Maven Central publication. +- Code cleanups, static analysis fixes, and dependency upgrades. + +## 2.1.0 + +- Introduced `GrammarDefinition` and `GrammarParser` following the Dart architecture. +- Migrated XML and Smalltalk example grammars to grammar definitions. +- Modernized internal collections, replacing legacy synchronized classes. + +## 2.0.0 + +- Major rewrite modernizing PetitParser for Java 8+ with lambda expressions. +- Package structure reorganization into `context`, `actions`, `combinators`, `primitive`, and `repeating`. +- Rebuilt character predicate hierarchy with optimized range checks and bitmask lookups. +- Added expression builder and composite grammar tools. diff --git a/README.md b/README.md index dcfd192..1f15a9c 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,26 @@ -PetitParser for Java -==================== +# PetitParser for Java [![Release Status](https://jitpack.io/v/petitparser/java-petitparser.svg)](https://jitpack.io/#petitparser/java-petitparser) [![Java CI](https://github.com/petitparser/java-petitparser/actions/workflows/maven.yml/badge.svg)](https://github.com/petitparser/java-petitparser/actions/workflows/maven.yml) [![GitHub Issues](https://img.shields.io/github/issues/petitparser/java-petitparser.svg)](https://github.com/petitparser/java-petitparser/issues) [![GitHub Forks](https://img.shields.io/github/forks/petitparser/java-petitparser.svg)](https://github.com/petitparser/java-petitparser/network) [![GitHub Stars](https://img.shields.io/github/stars/petitparser/java-petitparser.svg)](https://github.com/petitparser/java-petitparser/stargazers) -[![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/petitparser/java-petitparser/master/LICENSE) +[![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/petitparser/java-petitparser/main/LICENSE) Grammars for programming languages are traditionally specified statically. They are hard to compose and reuse due to ambiguities that inevitably arise. -PetitParser combines ideas -from [scannnerless parsing](https://en.wikipedia.org/wiki/Scannerless_parsing) -, [parser combinators](https://en.wikipedia.org/wiki/Parser_combinator) -, [parsing expression grammars](https://en.wikipedia.org/wiki/Parsing_expression_grammar) ( -PEG) and packrat parsers to model grammars and parsers as objects that can be +PetitParser combines ideas from [scannerless parsing](https://en.wikipedia.org/wiki/Scannerless_parsing), +[parser combinators](https://en.wikipedia.org/wiki/Parser_combinator), +[parsing expression grammars](https://en.wikipedia.org/wiki/Parsing_expression_grammar) (PEG), +and packrat parsers to model grammars and parsers as objects that can be reconfigured dynamically. -This library is open source, stable and well tested. Development happens +This library is open source, stable, and well tested. Development happens on [GitHub](https://github.com/petitparser/java-petitparser). Feel free to report issues or create a pull-request there. General questions are best asked -on [StackOverflow](http://stackoverflow.com/questions/tagged/petitparser+java). +on [StackOverflow](https://stackoverflow.com/questions/tagged/petitparser+java). - -Installation ------------- +## Installation To include the latest release in your Java project follow the instructions below. @@ -46,7 +42,7 @@ To depend on the core library, add the following dependency: com.github.petitparser.java-petitparser petitparser-core - 2.4.2 + 2.5.0 ``` @@ -56,7 +52,7 @@ To also include the example grammars, use the following dependency: com.github.petitparser java-petitparser - 2.4.2 + 2.5.0 ``` @@ -70,7 +66,6 @@ Alternatively, if you'd like the latest code clone the repository: ```bash git clone https://github.com/petitparser/java-petitparser.git cd java-petitparser -git checkout master ``` Then build with [Bazel](http://bazel.io/): @@ -111,8 +106,7 @@ To also include the example grammars, use the following dependency: Instructions for alternative build systems you can find on [Maven Central](https://search.maven.org/artifact/com.github.petitparser/petitparser-core) -Tutorial --------- +## Tutorial ### Writing a Simple Grammar @@ -135,12 +129,12 @@ If you look at the object `id` in the debugger, you'll notice that the code above builds a tree of parser objects: - `SequenceParser`: This parser accepts a sequence of parsers. - - `CharacterParser`: This parser accepts a single letter. - - `PossessiveRepeatingParser`: This parser accepts zero or more times + - `CharacterParser`: This parser accepts a single letter. + - `PossessiveRepeatingParser`: This parser accepts zero or more times another parser. - - `ChoiceParser`: This parser accepts a single word character. - - `CharacterParser`: This parser accepts a single letter. - - `CharacterParser`: This parser accepts a single digit. + - `ChoiceParser`: This parser accepts a single word character. + - `CharacterParser`: This parser accepts a single letter. + - `CharacterParser`: This parser accepts a single digit. ### Parsing Some Input @@ -157,9 +151,7 @@ parse result using `Success#get()`: ```java System.out.println(id1.get()); // ['y', ['e', 'a', 'h']] - System.out. - -println(id2.get()); // ['f', ['1', '2']] +System.out.println(id2.get()); // ['f', ['1', '2']] ``` While it seems odd to get these nested arrays with characters as a return value, @@ -171,13 +163,9 @@ answer and we can retrieve a descriptive error message using `Failure#getMessage()`: ```java -Result id3 = id.parse('123'); -System.out. - -println(id3.getMessage()); // "letter expected" - System.out. - -println(id3.getPosition()); // 0 +Result id3 = id.parse("123"); +System.out.println(id3.getMessage()); // "letter expected" +System.out.println(id3.getPosition()); // 0 ``` Trying to retrieve the parse result by calling `Failure#get()` would throw the @@ -189,9 +177,7 @@ helper method `Parser#accept(String)`: ```java System.out.println(id.accept("foo")); // true - System.out. - -println(id.accept("123")); // false +System.out.println(id.accept("123")); // false ``` ### Different Kinds of Parsers @@ -219,9 +205,12 @@ Parser id = letter().seq(word().star()); The next set of parsers are used to combine other parsers together: - `p1.seq(p2)` parses `p1` followed by `p2` (sequence). +- `p1.then(p2)` parses `p1` followed by `p2`, preserving types in a strongly-typed sequence (`SequenceParser2` to `SequenceParser9`). - `p1.or(p2)` parses `p1`, if that doesn't work parses `p2` (ordered choice). - `p.star()` parses `p` zero or more times. - `p.plus()` parses `p` one or more times. +- `p.starSeparated(s)` parses `p` separated by `s` zero or more times, returning a `SeparatedList`. +- `p.skip(before, after)` parses `before`, then `p`, then `after`, returning `p`. - `p.optional()` parses `p`, if possible. - `p.and()` parses `p`, but does not consume its input. - `p.not()` parses `p` and succeed when p fails, but does not consume its input. @@ -231,6 +220,7 @@ To attach an action or transformation to a parser we can use the following methods: - `p.map(value -> ...)` performs the transformation given the function. +- `p.then(...).map((a, b) -> ...)` performs strongly-typed multi-argument mapping without tuple unwrapping. - `p.pick(n)` returns the `n`-th element of the list `p` returns. - `p.flatten()` creates a string from the result of `p`. - `p.token()` creates a token from the result of `p`. @@ -243,13 +233,11 @@ Parser id = letter().seq(word().star()).flatten(); ``` To conveniently find all matches in a given input string you can -use `Parser#matchesSkipping(String)`: +use `Parser#matchesSkipping(String)` or lazily stream them via `Parser#matchesAsStream(String)`: ```java List matches = id.matchesSkipping("foo 123 bar4"); - System.out. - -println(matches); // ["foo", "bar4"] +System.out.println(matches); // ["foo", "bar4"] ``` These are the basic elements to build parsers. There are a few more well @@ -276,59 +264,19 @@ SettableParser term = SettableParser.undefined(); SettableParser prod = SettableParser.undefined(); SettableParser prim = SettableParser.undefined(); -term. - -set(prod.seq(of('+'). - -trim()). - -seq(term). - -map((List values) ->{ - return values. - -get(0) +values. - -get(2); -}). - -or(prod)); - prod. - -set(prim.seq(of('*'). - -trim()). - -seq(prod). - -map((List values) ->{ - return values. - -get(0) *values. - -get(2); -}). - -or(prim)); - prim. - -set((of('('). - -trim(). - -seq(term). - -seq(of(')'). - -trim())). - -map((List values) ->{ - return values. - -get(1); -}). - -or(number)); +term.set(prod.seq(of('+').trim()) + .seq(term) + .map((List values) -> values.get(0) + values.get(2)) + .or(prod)); +prod.set(prim.seq(of('*').trim()) + .seq(prod) + .map((List values) -> values.get(0) * values.get(2)) + .or(prim)); +prim.set(of('(').trim() + .seq(term) + .seq(of(')').trim()) + .map((List values) -> values.get(1)) + .or(number)); ``` To make sure that our parser consumes all input we wrap it with the `end()` @@ -341,14 +289,8 @@ Parser start = term.end(); That's it, now we can test our parser and evaluator: ```java -System.out.println(start.parse("1 + 2 * 3"). - -get()); // 7 - System.out. - -println(start.parse("(1 + 2) * 3"). - -get()); // 9 +System.out.println(start.parse("1 + 2 * 3").get()); // 7 +System.out.println(start.parse("(1 + 2) * 3").get()); // 9 ``` As an exercise we could extend the parser to also accept negative numbers and @@ -366,7 +308,7 @@ and prefix, postfix, left- and right-associative operators. The following code creates the empty expression builder: ```java -ExpressionBuilder builder = new ExpressionBuilder(); +ExpressionBuilder builder = new ExpressionBuilder<>(); ``` Then we define the operator-groups in descending precedence. The highest @@ -375,119 +317,38 @@ point numbers, not just integers. In the same group we add support for parenthesis: ```java -builder.group() - . - -primitive(digit(). - -plus(). - -seq(of('.') - . - -seq(digit(). - -plus()). - -optional()) - . - -flatten(). +ExpressionBuilder builder = new ExpressionBuilder<>(); -trim(). - -map(Double::parseDouble)) - . - -wrapper(of('('). - -trim(),of(')'). - -trim(), - ( -List values)->values. - -get(1)); +builder.group() + .primitive(digit().plus() + .seq(of('.').seq(digit().plus()).optional()) + .flatten() + .trim() + .map(Double::parseDouble)) + .wrapper(of('(').trim(), of(')').trim(), (left, value, right) -> value); ``` -Then come the normal arithmetic operators. Note, that the action blocks receive -both, the terms and the parsed operator in the order they appear in the parsed -input: +Then come the normal arithmetic operators. With the modern strongly-typed +`ExpressionBuilder`, operators accept typed lambda callbacks directly: ```java // negation is a prefix operator builder.group() - . - -prefix(of('-'). - -trim(), ( -List values)->-values. - -get(1)); + .prefix(of('-').trim(), (op, value) -> -value); // power is right-associative - builder. - -group() - . - -right(of('^'). - -trim(), ( -List values)->Math. - -pow(values.get(0),values. - -get(2))); - -// multiplication and addition are left-associative - builder. - -group() - . - -left(of('*'). - -trim(), ( -List values)->values. - -get(0) *values. - -get(2)) - . - -left(of('/'). - -trim(), ( -List values)->values. - -get(0) /values. - -get(2)); - builder. - -group() - . - -left(of('+'). - -trim(), ( -List values)->values. - -get(0) +values. - -get(2)) - . - -left(of('-'). - -trim(), ( -List values)->values. +builder.group() + .right(of('^').trim(), (left, op, right) -> Math.pow(left, right)); -get(0) -values. +// multiplication and division are left-associative +builder.group() + .left(of('*').trim(), (left, op, right) -> left * right) + .left(of('/').trim(), (left, op, right) -> left / right); -get(2)); +// addition and subtraction are left-associative +builder.group() + .left(of('+').trim(), (left, op, right) -> left + right) + .left(of('-').trim(), (left, op, right) -> left - right); ``` Finally we can build the parser: @@ -500,26 +361,17 @@ After executing the above code we get an efficient parser that correctly evaluates expressions like: ```java -parser.parse("-8"); // -8 -parser. - -parse("1+2*3"); // 7 -parser. - -parse("1*2+3"); // 5 -parser. - -parse("8/4/2"); // 1 -parser. - -parse("2^2^3"); // 256 +parser.parse("-8"); // -8 +parser.parse("1+2*3"); // 7 +parser.parse("1*2+3"); // 5 +parser.parse("8/4/2"); // 1 +parser.parse("2^2^3"); // 256 ``` You can find this example as test case here: [ExamplesTest.java](petitparser-core/src/test/java/org/petitparser/ExamplesTest.java) -Misc ----- +## Misc ### Examples @@ -551,6 +403,4 @@ implementations adopt best practises of the target language. ### License -The MIT License, -see [LICENSE](https://raw.githubusercontent.com/petitparser/java-petitparser/master/LICENSE) -. +The MIT License, see [LICENSE](https://raw.githubusercontent.com/petitparser/java-petitparser/main/LICENSE). diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..6c56eef --- /dev/null +++ b/TASKS.md @@ -0,0 +1,346 @@ +# PetitParser Java Parity Tasks + +Roadmap to bring `petitparser-core` to feature parity with the canonical Dart implementation while maintaining strict backward compatibility with existing code and adopting modern Java best practices (Java 21 baseline, standard functional interfaces, and stream idioms). + +--- + +## Compatibility, Native Design & Efficiency Principles + +### Strict Backward Compatibility +- Preserve all existing public methods, constructors, and classes. +- Downstream modules (`petitparser-json`, `petitparser-xml`, `petitparser-smalltalk`) and user code must compile and pass all tests without modification. +- Additive evolution: introduce new functionality alongside existing APIs (e.g. `starSeparated` returning `SeparatedList` without removing `separatedBy` returning `List`). +- When introducing generic parameters (`Parser`, `Result`, etc.), ensure raw-type usage compiles cleanly without warnings or breaking changes. + +### Native Java Idiomatic Design +- Avoid mechanical translations of Dart-specific constructs (e.g. extension methods, positional record tuples, operator overloads). +- Leverage standard Java paradigms: + - Fluent method chaining on `Parser` and builders. + - Standard functional interfaces (`java.util.function.Function`, `Predicate`, `BiFunction`, `Supplier`, `Consumer`). + - Java `Stream` and lazy `Iterable`/`Iterator` for streaming matching and graph traversals. + - Immutability for core state (`Context`, `Result`, `Token`, `SeparatedList`) using defensive copies and `Collections.unmodifiableList`. + - Null-safety checks using `Objects.requireNonNull(arg, "message")`. + +### High-Performance & Efficiency Goals +- **Zero-Allocation Fast Path**: Every parser subclass must implement an optimized `int fastParseOn(String buffer, int position)` that performs zero heap allocations (no `Context`, `Result`, or boxed wrapper instances) and returns `-1` on failure without throwing exceptions. +- **Direct String Slicing (Zero-List Lexing)**: Character repeaters (`starString()`, `plusString()`, `repeatString()`) must scan characters using primitive loop indexes on `buffer.charAt(i)` and extract the final string with a single native `buffer.substring(start, stop)` call, completely bypassing intermediate `List` allocations. +- **$O(1)$ Character Lookup Tables**: Specialize character predicates with boolean lookup tables (`boolean[256]`) or primitive bitmasks (`BitSet`/`long[]`) for ASCII/Latin-1 character sets, executing in constant time rather than branching or binary searching. +- **Range Merging Optimization**: Automatically sort and merge adjacent/overlapping character ranges during grammar construction so runtime checks execute the minimum possible comparisons. +- **HotSpot SIMD Intrinsics**: String literal parsers must leverage JVM-intrinsic methods like `String.startsWith(prefix, position)` and `String.regionMatches` to take advantage of vectorized CPU instructions. +- **Stateless Singleton Reuse**: Use static singleton instances for stateless parsers and predicates (e.g. `PositionParser`, `EpsilonParser`, `NewlineParser`, `ConstantCharPredicate.any()`, `DigitCharPredicate.INSTANCE`). +- **Lazy Stream Matching**: Matcher APIs must provide lazy `Stream` and `Iterable` pipelines to process matches on demand with minimal memory footprint. + +--- + +## Phase 1: Core Action, Primitive & Combinator Parsers + +- [x] **Task 1.1: Marker & Lifecycle Interfaces** + - Create `org.petitparser.parser.combinators.SequentialParser` marker interface. + - Implement `SequentialParser` on `SequenceParser` and `TrimmingParser`. + - Create `org.petitparser.parser.combinators.ResolvableParser` with `Parser resolve()`. + - Implement `ResolvableParser` on `SettableParser`. + +- [x] **Task 1.2: WhereParser & Filtering** + - Implement `org.petitparser.parser.actions.WhereParser` with `java.util.function.Predicate` and custom error message / failure factory `BiFunction`. + - Add `where(Predicate)`, `where(Predicate, String)`, and `where(Predicate, BiFunction)` to `Parser`. + - Implement zero-allocation fast-parse, copy, and equality methods. + +- [x] **Task 1.3: SkipParser & Delimiters** + - Implement `org.petitparser.parser.combinators.SkipParser` implementing `SequentialParser`. + - Add `skip(Parser before, Parser after)` to `Parser`. + - Implement zero-allocation fast-parse, copy, child replacement, and equality methods. + +- [x] **Task 1.4: LabelParser & Debugging** + - Implement `org.petitparser.parser.combinators.LabelParser` wrapping a delegate with a string label. + - Add `labeled(String label)` to `Parser`. + - Override `toString()` to display `delegate.toString() + "[" + label + "]"`. + +- [x] **Task 1.5: PositionParser** + - Implement `org.petitparser.parser.primitive.PositionParser` returning `context.getPosition()`. + - Provide a reusable static singleton `PositionParser.INSTANCE`. + - Add static factory `Parser.position()`. + - Implement zero-allocation fast-parse returning position unchanged. + +- [x] **Task 1.6: NewlineParser** + - Implement `org.petitparser.parser.primitive.NewlineParser` matching `\n`, `\r\n`, and `\r`. + - Add static factories `Parser.newline()` and `Parser.newline(String message)`. + - Implement branch-optimized zero-allocation `fastParseOn`. + +- [x] **Task 1.7: Epsilon Value Support & Primitives** + - Extend `EpsilonParser` to store an optional result value `value` (defaulting to `null`). + - Provide a reusable static singleton `EpsilonParser.INSTANCE` for `null`. + - Add static factories `Parser.epsilon()` and `Parser.epsilon(Object value)`. + - Add static factories `Parser.failure()` and `Parser.failure(String message)`. + - Add `Parser.constant(Object value)` replacing results with a constant value. + +- [x] **Task 1.8: Token & Context Formatting Utilities** + - Add `Token.positionString(String buffer, int position)` returning `"line:column"`. + - Add `Token.join(Iterable tokens)` combining adjacent tokens from the same buffer. + - Add `Context.toPositionString()` returning `"line:column"`. + +- [x] **Task 1.9: Phase 1 Parallel Unit Tests** + - Create test classes under `src/test/java/org/petitparser/`: + - `org.petitparser.parser.actions.WhereParserTest` + - `org.petitparser.parser.combinators.SkipParserTest` + - `org.petitparser.parser.combinators.LabelParserTest` + - `org.petitparser.parser.primitive.PositionParserTest` + - `org.petitparser.parser.primitive.NewlineParserTest` + - `org.petitparser.parser.primitive.EpsilonParserTest` + - `org.petitparser.context.TokenTest` (augment with `join` and `positionString` tests) + - Ensure 100% test coverage for all new lines and branches. + +--- + +## Phase 2: High-Performance Character Repeaters & Separated Sequences + +- [x] **Task 2.1: RepeatingCharacterParser (Zero-List Lexing)** + - Implement `org.petitparser.parser.repeating.RepeatingCharacterParser` directly returning a `String` via `buffer.substring(start, position)` without intermediate `List` allocations. + - Add `starString()`, `starString(String message)` to `Parser`. + - Add `plusString()`, `plusString(String message)` to `Parser`. + - Add `timesString(int count)`, `timesString(int count, String message)` to `Parser`. + - Add `repeatString(int min, int max)`, `repeatString(int min, int max, String message)` to `Parser`. + - Auto-specialize when called on `CharacterParser` to instantiate `RepeatingCharacterParser`. + +- [x] **Task 2.2: SeparatedList Data Structure** + - Implement immutable `org.petitparser.parser.repeating.SeparatedList` holding `elements` and `separators`. + - Implement `getSequential()` returning alternating interleaved elements and separators. + - Implement functional folding: `foldLeft(FoldFunction callback)` and `foldRight(FoldFunction callback)`. + - Implement `equals`, `hashCode`, `toString`, and `Iterable`. + +- [x] **Task 2.3: SeparatedRepeatingParser** + - Implement `org.petitparser.parser.repeating.SeparatedRepeatingParser` implementing `SequentialParser`. + - Add `starSeparated(Parser separator)` to `Parser`. + - Add `plusSeparated(Parser separator)` to `Parser`. + - Add `timesSeparated(Parser separator, int count)` to `Parser`. + - Add `repeatSeparated(Parser separator, int min, int max)` to `Parser`. + - Retain existing `separatedBy` and `delimitedBy` returning `List` for full backward compatibility. + +- [x] **Task 2.4: Phase 2 Parallel Unit Tests** + - Create test classes: + - `org.petitparser.parser.repeating.RepeatingCharacterParserTest` + - `org.petitparser.parser.repeating.SeparatedListTest` + - `org.petitparser.parser.repeating.SeparatedRepeatingParserTest` + - Test zero, single, multiple repetitions, fast-parse, and folding operations. + +--- + +## Phase 3: Strongly Typed Sequences, Tuples & Action Combinators + +- [x] **Task 3.1: Dual-Nature Tuple Hierarchy (`Tuple2` to `Tuple9`)** + - Implement immutable `org.petitparser.utils.tuples.Tuple2` through `Tuple9` extending `java.util.AbstractList`. + - Provide strongly-typed accessors: `tuple.first()`, `tuple.second()`, `tuple.third()`, etc. + - Implement `AbstractList` contract (`size()`, `get(int index)`), allowing tuples to be treated directly as `List` for 100% backward compatibility with legacy tests and downstream consumers (`tuple.equals(Arrays.asList(...)) == true`). + - Implement zero-allocation accessors and structural equality (`equals`, `hashCode`, `toString`). + +- [x] **Task 3.2: Multi-Arity Functional Interfaces** + - Create standard functional interfaces in `org.petitparser.utils.functions`: + - `Function3`, `Function4`, ... `Function9<...>`. + - Ensure compatibility with Java `BiFunction` for arity-2 sequences. + +- [x] **Task 3.3: Typed Sequence Combinators (`SequenceParser2` to `SequenceParser9`)** + - Implement `SequenceParser2` through `SequenceParser9<...>` implementing `SequentialParser`. + - Add fluent `.then(Parser next)` on `Parser` and sequence parsers, automatically flattening into `SequenceParser3`, `SequenceParser4`, etc. up to 9 elements. + - Add strongly-typed `.map(BiFunction function)` on `SequenceParser2`. + - Add strongly-typed `.map(Function3<...>)` through `.map(Function9<...>)` on `SequenceParser3` through `SequenceParser9`, eliminating the need for users to manually unpack tuple or list elements. + - Implement zero-allocation `fastParseOn`, `copy`, and equality methods. + +- [x] **Task 3.4: Static Sequence Factories** + - Add static factories `Parser.seq(Parser p1, Parser p2)` returning `SequenceParser2`. + - Add static factories `Parser.seq(p1, p2, p3)` through `Parser.seq(p1, ..., p9)` returning corresponding typed sequence parsers. + - Retain existing `Parser.seq(Parser... parsers)` and instance method `seq(Parser other)` returning `SequenceParser` (`Parser>`) for backward compatibility. + +- [x] **Task 3.5: Type Extraction, Casting & Permutation** + - Implement `org.petitparser.parser.actions.CastParser` with `cast()` and `cast(Class clazz)` on `Parser`. + - Implement `castList(Class clazz)` on `Parser>`. + - Implement `pick(int index)` with negative indexing support, specialized on `RepeatingParser` to return `Parser`. + - Implement `permute(int... indices)` returning reordered `Tuple` or `List`. + +- [x] **Task 3.6: Variance-Friendly Alternatives (`or` & `orWiden`)** + - Add `or(Parser other)` to `Parser` to preserve precise local type inference when using Java `var`. + - Add ` Parser orWiden(Parser other)` and static `Parser.or(Parser... parsers)` for explicit widening across differing types (e.g. `Integer` and `Double` into `Number`). + +- [x] **Task 3.7: Strongly-Typed Grammar Definitions** + - Introduce `org.petitparser.tools.Production` typed token key: + - Factory `Production.of(String name)` / `GrammarDefinition.production(String name)`. + - Add typed `def(Production production, Parser parser)`. + - Add typed `ref(Production production)` returning `Parser`. + - Retain string-based `def(String, Parser)` and enhance `ref(String)` with ` Parser ref(String name)` for seamless existing code compatibility. + +- [x] **Task 3.8: Phase 3 Parallel Unit Tests** + - Create test classes: + - `org.petitparser.utils.tuples.TupleTest` (verifying typed access and `List` contract) + - `org.petitparser.parser.combinators.SequenceParserNTest` (verifying `then()`, `map()`, arity 2-9, fast-parse) + - `org.petitparser.parser.actions.CastParserTest` + - `org.petitparser.parser.actions.PickParserTest` + - `org.petitparser.tools.TypedGrammarDefinitionTest` + +--- + +## Phase 4: Indentation-Sensitive Parsing + +- [x] **Task 4.1: Indent Engine** + - Implement `org.petitparser.tools.Indent` class managing an indentation stack and current indent string. + - Implement `same()` matching current indentation level. + - Implement `increase()` requiring deeper indentation and pushing to stack. + - Implement `decrease()` popping indentation from stack. + - Implement `during(Parser inner)` with transactional rollback on parse failure and choice backtracking. + +- [x] **Task 4.2: Phase 4 Parallel Unit Tests** + - Create `org.petitparser.tools.IndentTest`: + - Success restoring outer indentation. + - Failure rolling back indentation stack. + - Nested block scopes. + - Choice rollback trying alternate indented branches. + - Custom indentation token parsers (tabs vs spaces). + +--- + +--- + +## Phase 5: Character Predicate AST & Unicode Code Points + +- [x] **Task 5.1: Hybrid CharacterPredicate Hierarchy** + - Preserve `@FunctionalInterface CharacterPredicate` interface (`test(char)`) for backward compatibility with user lambdas. + - Introduce concrete AST classes implementing `CharacterPredicate` and `isEqualTo`: + - `SingleCharPredicate`, `RangeCharPredicate`, `RangesCharPredicate` (binary search on primitive arrays). + - `LookupCharPredicate` ($O(1)$ boolean array / `BitSet` lookup table for ASCII/Latin-1). + - `NotCharPredicate` (negated delegate). + - Reusable singleton constants: `DigitCharPredicate`, `LetterCharPredicate`, `LowercaseCharPredicate`, `UppercaseCharPredicate`, `WhitespaceCharPredicate`, `WordCharPredicate`, `ConstantCharPredicate.any`, `ConstantCharPredicate.none`. + - Update `CharacterParser` factories (`digit()`, `letter()`, etc.) to use AST singletons so `p.isEqualTo(p)` succeeds across separate calls. + +- [x] **Task 5.2: Range Merging & Character Optimization** + - Implement `optimizedRanges(List ranges)`: + - Sort ranges by start and stop. + - Merge adjacent and overlapping ranges to minimize runtime branch tests. + - Choose optimal predicate ($O(1)$ Lookup table, Single, Range, or Ranges with binary search). + - Implement `optimizedString(String string, boolean ignoreCase)`. + +- [x] **Task 5.3: StringParser SIMD & Equality Fix** + - Refactor `StringParser.ofIgnoringCase` to avoid non-comparable method reference lambdas (`value::equalsIgnoreCase`). + - Implement `StringIgnoreCaseParser` storing the literal string and comparing literals in `hasEqualProperties`. + - Use `String.regionMatches` and `String.startsWith` for HotSpot-intrinsic performance. + +- [x] **Task 5.4: Unicode Code Points** + - Implement `UnicodeCharacterParser` decoding UTF-16 surrogate pairs into 21-bit code points ($0 \dots \text{0x10FFFF}$). + - Add `unicode` parameter / flag to character primitives (`any`, `char`, `pattern`, `anyOf`, `noneOf`). + +- [x] **Task 5.5: Phase 5 Parallel Unit Tests** + - Create tests: + - `org.petitparser.parser.primitive.CharacterPredicateAstTest` + - `org.petitparser.parser.primitive.UnicodeCharacterParserTest` + - Verify structural equality of character parsers across all predicate variations. + +--- + +## Phase 6: ExpressionBuilder Modernization + +- [x] **Task 6.1: ExpressionBuilder Enhancements** + - Introduce generic typing `ExpressionBuilder` and `ExpressionGroup`. + - Add `primitive(Parser parser)` directly on `ExpressionBuilder`. + - Add `loopback` getter on `ExpressionBuilder`. + - Add `optional(Object value)` on `ExpressionGroup`. + - Refactor binary left/right operator builders to use `plusSeparated` and `SeparatedList.foldLeft` / `foldRight`. + - Add typed functional callbacks (e.g. ternary `(left, op, right) -> result` via `Function3`) while preserving existing `List` callbacks for compatibility. + +- [x] **Task 6.2: Phase 6 Parallel Unit Tests** + - Augment `org.petitparser.tools.ExpressionBuilderTest` to test: + - Builder-level primitives. + - Optional expression groups. + - Typed operator callbacks. + +--- + +## Phase 7: Grammar Reflection & Analyzer + +- [x] **Task 7.1: Grammar Graph Traversal & Analyzer Base** + - Implement `org.petitparser.utils.Analyzer` leveraging standard Java collections and `Stream`: + - Reachable parsers discovery (`parsers`). + - Deep children set cache (`allChildren(parser)`). + - Graph path search (`findPath`, `findPathTo`, `findAllPaths`, `findAllPathsTo`). + +- [x] **Task 7.2: Grammar Property Computation** + - Implement nullability fixed-point analysis (`isNullable(parser)`). + - Implement FIRST-set calculation (`firstSet(parser)`): terminal parsers that can appear first, taking `SequentialParser` into account. + - Implement FOLLOW-set calculation (`followSet(parser)`): terminal parsers that can immediately succeed `parser`. + - Implement cycle detection (`cycleSet(parser)`). + +- [x] **Task 7.3: Grammar Reference Inlining** + - Implement `resolve(Parser parser)` resolving all `ResolvableParser` references into direct cycles/graphs without delegate overhead. + +- [x] **Task 7.4: Phase 7 Parallel Unit Tests** + - Create `org.petitparser.utils.AnalyzerTest`: + - First-set and follow-set validation on LL/LR grammar definitions. + - Nullable chain detection. + - Path searching between arbitrary nodes. + - Resolvable reference inlining. + +--- + +## Phase 8: Grammar Linter + +- [x] **Task 8.1: Linter Engine Architecture** + - Implement `org.petitparser.utils.linter.LinterRule`, `LinterIssue`, `LinterType` (info, warning, error). + - Implement `linter(Parser root, ...)` executing active rules using `Analyzer`. + +- [x] **Task 8.2: 13 Linter Rules Implementation** + - [x] `CharacterRepeaterRule`: Identifies `.star().flatten()` on character parsers and suggests `starString()`. + - [x] `DuplicateParserRule`: Identifies duplicate structurally equal parser instances in grammar graph. + - [x] `LeftRecursionRule`: Identifies left-recursive loops that lead to infinite recursion. + - [x] `NestedChoiceRule`: Identifies nested choice combinators. + - [x] `NullableRepeaterRule`: Identifies repeating parsers over nullable delegates that lead to infinite loops. + - [x] `OverlappingChoiceRule`: Identifies choices where a prefix choice shadows a later choice. + - [x] `RepeatedChoiceRule`: Identifies duplicate branches in a choice parser. + - [x] `UnnecessaryFlattenRule`: Identifies `flatten()` on parsers already producing Strings. + - [x] `UnnecessaryResolvableRule`: Identifies unresolved resolvable wrappers. + - [x] `UnoptimizedFlattenRule`: Identifies `flatten()` without error messages preventing switch to fast parsing mode. + - [x] `UnreachableChoiceRule`: Identifies choices located after unconditional matchers. + - [x] `UnresolvedSettableRule`: Identifies `SettableParser` left in undefined state. + - [x] `UnusedResultRule`: Identifies complex sub-parses whose results are discarded (e.g. inner actions inside `flatten()`). + +- [x] **Task 8.3: Phase 8 Parallel Unit Tests** + - Create `org.petitparser.utils.linter.LinterTest` verifying positive and negative triggers for each of the 13 rules. + +--- + +## Phase 9: Extended Optimizer + +- [x] **Task 9.1: Extensible OptimizeRule Framework** + - Define `OptimizeRule` interface and refactor `Optimizer` to use modular rules. + - Keep `RemoveDelegate` and `RemoveDuplicate` rules. + +- [x] **Task 9.2: New Optimizer Rules** + - Implement `FlattenChoiceRule`: Flattens nested choices `[a, [b, c]]` into `[a, b, c]`. + - Implement `CharacterRepeaterRule`: Transforms `FlattenParser(PossessiveRepeatingParser(CharacterParser))` into `RepeatingCharacterParser`. + +- [x] **Task 9.3: Phase 9 Parallel Unit Tests** + - Augment `org.petitparser.utils.OptimizerTest` to cover choice flattening and character repeater rewrites. + +--- + +## Phase 10: Debugger & Matcher Enhancements + +- [x] **Task 10.1: Progress Stepper** + - Implement `org.petitparser.utils.Progress`: + - `progress(Parser root, Consumer observer)` visual execution debugger. + - `ProgressFrame` capturing parser, context, position, and backtracking events. + +- [x] **Task 10.2: Matcher Offset & Lazy Streams** + - Enhance `accept(String input, int start)` with start offset. + - Add lazy `matchesAsStream(String input, int start)` returning `java.util.stream.Stream` backed by a custom `Spliterator`. + - Add lazy `matches(String input, int start)` returning `Iterable`. + +- [x] **Task 10.3: Phase 10 Parallel Unit Tests** + - Create `org.petitparser.utils.ProgressTest` and `org.petitparser.MatcherTest`. + +--- + +## Phase 11: End-to-End Verification & Backward Compatibility + +- [x] **Task 11.1: Downstream Module Compatibility Verification** + - Verify `petitparser-json`, `petitparser-xml`, and `petitparser-smalltalk` compile and pass tests without modifications. + - Ensure raw-type usage compiles cleanly without errors or breaking changes for existing code. + +- [x] **Task 11.2: JMH Benchmark Suite** + - Measure throughput and allocation rate of `RepeatingCharacterParser` vs `.star().flatten()`. + - Measure $O(1)$ Lookup table predicates vs chained range predicates. + - Measure `fastParseOn` zero-allocation performance against Dart baseline. diff --git a/jitpack.yml b/jitpack.yml index 57db296..3d3e7cc 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1 +1,4 @@ -jdk: openjdk17 \ No newline at end of file +jdk: + - openjdk21 +before_install: + - rm -rf ~/.m2/wrapper diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..e65b6e4 --- /dev/null +++ b/mvnw @@ -0,0 +1,299 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL M2_HOME || : + export MAVEN_HOME + export MAVEN_SKIP_RC=true + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ] && [ -f "$MAVEN_HOME/bin/$MVN_CMD" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi +rm -rf -- "$MAVEN_HOME" + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +rm -rf -- "$MAVEN_HOME" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/petitparser-core/pom.xml b/petitparser-core/pom.xml index e6b1244..1504b1a 100644 --- a/petitparser-core/pom.xml +++ b/petitparser-core/pom.xml @@ -7,7 +7,7 @@ com.github.petitparser petitparser - 2.4.2 + 2.5.0 ../pom.xml diff --git a/petitparser-core/src/main/java/module-info.java b/petitparser-core/src/main/java/module-info.java index d410602..11265b7 100644 --- a/petitparser-core/src/main/java/module-info.java +++ b/petitparser-core/src/main/java/module-info.java @@ -7,4 +7,8 @@ exports org.petitparser.parser.repeating; exports org.petitparser.tools; exports org.petitparser.utils; -} \ No newline at end of file + exports org.petitparser.utils.functions; + exports org.petitparser.utils.linter; + exports org.petitparser.utils.optimizer; + exports org.petitparser.utils.tuples; +} diff --git a/petitparser-core/src/main/java/org/petitparser/context/Context.java b/petitparser-core/src/main/java/org/petitparser/context/Context.java index 7d845c1..fe5923b 100644 --- a/petitparser-core/src/main/java/org/petitparser/context/Context.java +++ b/petitparser-core/src/main/java/org/petitparser/context/Context.java @@ -78,9 +78,15 @@ public Failure failure(String message, int position) { return new Failure(buffer, position, message); } + /** + * Returns the current line:column position in the buffer. + */ + public String toPositionString() { + return Token.positionString(buffer, position); + } + @Override public String toString() { - int[] tuple = Token.lineAndColumnOf(buffer, position); - return getClass().getSimpleName() + "[" + tuple[0] + ":" + tuple[1] + "]"; + return getClass().getSimpleName() + "[" + toPositionString() + "]"; } } diff --git a/petitparser-core/src/main/java/org/petitparser/context/Token.java b/petitparser-core/src/main/java/org/petitparser/context/Token.java index b454b5b..34e5c76 100644 --- a/petitparser-core/src/main/java/org/petitparser/context/Token.java +++ b/petitparser-core/src/main/java/org/petitparser/context/Token.java @@ -2,6 +2,9 @@ import org.petitparser.parser.Parser; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -104,10 +107,16 @@ public int getColumn() { return lineAndColumnOf(buffer, start)[1]; } + /** + * Returns a string representation of the starting position in the format "line:column". + */ + public String toPositionString() { + return positionString(buffer, start); + } + @Override public String toString() { - int[] tuple = lineAndColumnOf(buffer, start); - return "Token[" + tuple[0] + ":" + tuple[1] + "]: " + value; + return "Token[" + positionString(buffer, start) + "]: " + value; } @Override @@ -161,4 +170,82 @@ public static int[] lineAndColumnOf(String buffer, int position) { } return new int[]{line, position - offset + 1}; } + + /** + * Returns a human readable string representing the {@code position} index in a + * {@code buffer} formatted as {@code "line:column"}. + */ + public static String positionString(String buffer, int position) { + int[] lineAndColumn = lineAndColumnOf(buffer, position); + return lineAndColumn[0] + ":" + lineAndColumn[1]; + } + + /** + * Combines multiple tokens into a single token with the list of their values. + * + * @param tokens the tokens to combine. + * @return a single combined token. + * @throws IllegalArgumentException if tokens is empty or tokens do not share the same buffer. + */ + public static Token join(Token... tokens) { + return join(Arrays.asList(Objects.requireNonNull(tokens, "Undefined tokens"))); + } + + /** + * Combines multiple tokens into a single token with the list of their values. + * + * @param tokens the tokens to combine. + * @return a single combined token. + * @throws IllegalArgumentException if tokens is empty or tokens do not share the same buffer. + */ + public static Token join(Iterable tokens) { + Iterator iterator = Objects.requireNonNull(tokens, "Undefined tokens").iterator(); + if (!iterator.hasNext()) { + throw new IllegalArgumentException("Require at least one token"); + } + Token first = Objects.requireNonNull(iterator.next(), "Undefined token"); + List values = new ArrayList<>(); + values.add(first.getValue()); + String buffer = first.getBuffer(); + int start = first.getStart(); + int stop = first.getStop(); + while (iterator.hasNext()) { + Token current = Objects.requireNonNull(iterator.next(), "Undefined token"); + if (!Objects.equals(buffer, current.getBuffer())) { + throw new IllegalArgumentException("Tokens do not use the same buffer"); + } + values.add(current.getValue()); + start = Math.min(start, current.getStart()); + stop = Math.max(stop, current.getStop()); + } + return new Token(buffer, start, stop, values); + } + + /** + * Combines multiple tokens into a single token with a custom value. + * + * @param tokens the tokens to combine. + * @param value the custom value of the resulting token. + * @return a single combined token. + * @throws IllegalArgumentException if tokens is empty or tokens do not share the same buffer. + */ + public static Token join(Iterable tokens, Object value) { + Iterator iterator = Objects.requireNonNull(tokens, "Undefined tokens").iterator(); + if (!iterator.hasNext()) { + throw new IllegalArgumentException("Require at least one token"); + } + Token first = Objects.requireNonNull(iterator.next(), "Undefined token"); + String buffer = first.getBuffer(); + int start = first.getStart(); + int stop = first.getStop(); + while (iterator.hasNext()) { + Token current = Objects.requireNonNull(iterator.next(), "Undefined token"); + if (!Objects.equals(buffer, current.getBuffer())) { + throw new IllegalArgumentException("Tokens do not use the same buffer"); + } + start = Math.min(start, current.getStart()); + stop = Math.max(stop, current.getStop()); + } + return new Token(buffer, start, stop, value); + } } diff --git a/petitparser-core/src/main/java/org/petitparser/parser/MatchesIterable.java b/petitparser-core/src/main/java/org/petitparser/parser/MatchesIterable.java new file mode 100644 index 0000000..031e001 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/MatchesIterable.java @@ -0,0 +1,75 @@ +package org.petitparser.parser; + +import java.util.Iterator; +import java.util.Objects; +import java.util.Spliterator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * An {@link Iterable} that lazily produces match results of a {@link Parser}. + */ +public class MatchesIterable implements Iterable { + + private final Parser parser; + private final String input; + private final int start; + private final boolean overlapping; + + /** + * Constructs an {@link Iterable} producing matches of {@code parser} on {@code input}. + * + * @param parser the parser to match + * @param input the input string to match against + * @param start the starting offset within {@code input} + * @param overlapping whether to produce overlapping matches + */ + public MatchesIterable(Parser parser, String input, int start, boolean overlapping) { + this.parser = Objects.requireNonNull(parser, "parser must not be null"); + this.input = Objects.requireNonNull(input, "input must not be null"); + if (start < 0) { + throw new IndexOutOfBoundsException("start offset must be non-negative: " + start); + } + this.start = start; + this.overlapping = overlapping; + } + + @Override + public Iterator iterator() { + return new MatchesIterator<>(parser, input, start, overlapping); + } + + @Override + public Spliterator spliterator() { + return new MatchesSpliterator<>(parser, input, start, overlapping); + } + + /** + * Returns a sequential {@link Stream} over the elements in this {@link Iterable}. + */ + public Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + public Parser getParser() { + return parser; + } + + public String getInput() { + return input; + } + + public int getStart() { + return start; + } + + public boolean isOverlapping() { + return overlapping; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[parser=" + parser + ", start=" + start + + ", overlapping=" + overlapping + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/MatchesIterator.java b/petitparser-core/src/main/java/org/petitparser/parser/MatchesIterator.java new file mode 100644 index 0000000..66071e5 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/MatchesIterator.java @@ -0,0 +1,109 @@ +package org.petitparser.parser; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * An {@link Iterator} that lazily produces match results of a {@link Parser}. + */ +public class MatchesIterator implements Iterator { + + private final Parser parser; + private final String input; + private final int start; + private final boolean overlapping; + private int current; + private T next; + private boolean hasNext; + private boolean advanced; + + /** + * Constructs an {@link Iterator} producing matches of {@code parser} on {@code input}. + * + * @param parser the parser to match + * @param input the input string to match against + * @param start the starting offset within {@code input} + * @param overlapping whether to produce overlapping matches + */ + public MatchesIterator(Parser parser, String input, int start, boolean overlapping) { + this.parser = Objects.requireNonNull(parser, "parser must not be null"); + this.input = Objects.requireNonNull(input, "input must not be null"); + if (start < 0) { + throw new IndexOutOfBoundsException("start offset must be non-negative: " + start); + } + this.start = start; + this.current = start; + this.overlapping = overlapping; + } + + private void advance() { + while (current <= input.length()) { + Result result = parser.parseOn(new Context(input, current)); + if (result.isFailure()) { + current++; + } else { + next = result.get(); + hasNext = true; + if (overlapping || current == result.getPosition()) { + current++; + } else { + current = result.getPosition(); + } + advanced = true; + return; + } + } + next = null; + hasNext = false; + advanced = true; + } + + @Override + public boolean hasNext() { + if (!advanced) { + advance(); + } + return hasNext; + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + T result = next; + next = null; + advanced = false; + return result; + } + + public Parser getParser() { + return parser; + } + + public String getInput() { + return input; + } + + public int getStart() { + return start; + } + + public int getCurrent() { + return current; + } + + public boolean isOverlapping() { + return overlapping; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[parser=" + parser + ", start=" + start + + ", current=" + current + ", overlapping=" + overlapping + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/MatchesSpliterator.java b/petitparser-core/src/main/java/org/petitparser/parser/MatchesSpliterator.java new file mode 100644 index 0000000..6f902d6 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/MatchesSpliterator.java @@ -0,0 +1,122 @@ +package org.petitparser.parser; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; + +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.Consumer; + +/** + * A {@link Spliterator} that lazily produces match results of a {@link Parser}. + */ +public class MatchesSpliterator implements Spliterator { + + private final Parser parser; + private final String input; + private final int start; + private final boolean overlapping; + private int current; + + /** + * Constructs a {@link Spliterator} producing matches of {@code parser} on {@code input}. + * + * @param parser the parser to match + * @param input the input string to match against + * @param start the starting offset within {@code input} + * @param overlapping whether to produce overlapping matches + */ + public MatchesSpliterator(Parser parser, String input, int start, boolean overlapping) { + this.parser = Objects.requireNonNull(parser, "parser must not be null"); + this.input = Objects.requireNonNull(input, "input must not be null"); + if (start < 0) { + throw new IndexOutOfBoundsException("start offset must be non-negative: " + start); + } + this.start = start; + this.current = start; + this.overlapping = overlapping; + } + + @Override + public boolean tryAdvance(Consumer action) { + Objects.requireNonNull(action, "action must not be null"); + while (current <= input.length()) { + Result result = parser.parseOn(new Context(input, current)); + if (result.isFailure()) { + current++; + } else { + int end = result.getPosition(); + T value = result.get(); + if (overlapping || current == end) { + current++; + } else { + current = end; + } + action.accept(value); + return true; + } + } + return false; + } + + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action, "action must not be null"); + while (current <= input.length()) { + Result result = parser.parseOn(new Context(input, current)); + if (result.isFailure()) { + current++; + } else { + int end = result.getPosition(); + T value = result.get(); + if (overlapping || current == end) { + current++; + } else { + current = end; + } + action.accept(value); + } + } + } + + @Override + public Spliterator trySplit() { + return null; + } + + @Override + public long estimateSize() { + return Long.MAX_VALUE; + } + + @Override + public int characteristics() { + return Spliterator.ORDERED; + } + + public Parser getParser() { + return parser; + } + + public String getInput() { + return input; + } + + public int getStart() { + return start; + } + + public int getCurrent() { + return current; + } + + public boolean isOverlapping() { + return overlapping; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[parser=" + parser + ", start=" + start + + ", current=" + current + ", overlapping=" + overlapping + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/Parser.java b/petitparser-core/src/main/java/org/petitparser/parser/Parser.java index e9c0fef..c555c9a 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/Parser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/Parser.java @@ -4,24 +4,59 @@ import org.petitparser.context.Result; import org.petitparser.context.Token; import org.petitparser.parser.actions.ActionParser; +import org.petitparser.parser.actions.CastListParser; +import org.petitparser.parser.actions.CastParser; import org.petitparser.parser.actions.ContinuationParser; import org.petitparser.parser.actions.FlattenParser; +import org.petitparser.parser.actions.PermuteParser; +import org.petitparser.parser.actions.PickParser; import org.petitparser.parser.actions.TokenParser; import org.petitparser.parser.actions.TrimmingParser; +import org.petitparser.parser.actions.WhereParser; import org.petitparser.parser.combinators.AndParser; import org.petitparser.parser.combinators.ChoiceParser; import org.petitparser.parser.combinators.EndOfInputParser; +import org.petitparser.parser.combinators.LabelParser; import org.petitparser.parser.combinators.NotParser; import org.petitparser.parser.combinators.OptionalParser; import org.petitparser.parser.combinators.SequenceParser; +import org.petitparser.parser.combinators.SequenceParser2; +import org.petitparser.parser.combinators.SequenceParser3; +import org.petitparser.parser.combinators.SequenceParser4; +import org.petitparser.parser.combinators.SequenceParser5; +import org.petitparser.parser.combinators.SequenceParser6; +import org.petitparser.parser.combinators.SequenceParser7; +import org.petitparser.parser.combinators.SequenceParser8; +import org.petitparser.parser.combinators.SequenceParser9; import org.petitparser.parser.combinators.SettableParser; +import org.petitparser.parser.combinators.SkipParser; import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.EpsilonParser; +import org.petitparser.parser.primitive.FailureParser; +import org.petitparser.parser.primitive.NewlineParser; +import org.petitparser.parser.primitive.PositionParser; import org.petitparser.parser.repeating.GreedyRepeatingParser; import org.petitparser.parser.repeating.LazyRepeatingParser; import org.petitparser.parser.repeating.PossessiveRepeatingParser; import org.petitparser.parser.repeating.RepeatingParser; +import org.petitparser.parser.repeating.SeparatedRepeatingParser; +import org.petitparser.utils.tuples.Tuple2; +import org.petitparser.utils.tuples.Tuple3; +import org.petitparser.utils.tuples.Tuple4; +import org.petitparser.utils.tuples.Tuple5; +import org.petitparser.utils.tuples.Tuple6; +import org.petitparser.utils.tuples.Tuple7; +import org.petitparser.utils.tuples.Tuple8; +import org.petitparser.utils.tuples.Tuple9; import org.petitparser.utils.FailureJoiner; import org.petitparser.utils.Functions; +import org.petitparser.utils.functions.Function3; +import org.petitparser.utils.functions.Function4; +import org.petitparser.utils.functions.Function5; +import org.petitparser.utils.functions.Function6; +import org.petitparser.utils.functions.Function7; +import org.petitparser.utils.functions.Function8; +import org.petitparser.utils.functions.Function9; import java.util.ArrayList; import java.util.Collections; @@ -29,7 +64,11 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import static org.petitparser.parser.primitive.CharacterParser.any; @@ -73,14 +112,36 @@ public int fastParseOn(String buffer, int position) { * Returns the parse result of the {@code input}. */ public Result parse(String input) { - return parseOn(new Context(input, 0)); + return parse(input, 0); + } + + /** + * Returns the parse result of the {@code input} starting at {@code start}. + */ + public Result parse(String input, int start) { + Objects.requireNonNull(input, "input must not be null"); + if (start < 0 || start > input.length()) { + return new Context(input, Math.max(0, Math.min(start, input.length()))) + .failure("Index out of bounds: " + start); + } + return parseOn(new Context(input, start)); } /** * Tests if the {@code input} can be successfully parsed. */ public boolean accept(String input) { - return fastParseOn(input, 0) >= 0; + return accept(input, 0); + } + + /** + * Tests if the {@code input} can be successfully parsed starting at {@code start}. + */ + public boolean accept(String input, int start) { + if (input == null || start < 0 || start > input.length()) { + return false; + } + return fastParseOn(input, start) >= 0; } /** @@ -94,6 +155,22 @@ public List matches(String input) { return (List) list; } + /** + * Returns a lazy iterable of all successful overlapping parses of the {@code input} + * starting at {@code start}. + */ + public Iterable matches(String input, int start) { + return matches(input, start, true); + } + + /** + * Returns a lazy iterable of all successful parses of the {@code input} + * starting at {@code start}, optionally overlapping. + */ + public Iterable matches(String input, int start, boolean overlapping) { + return new MatchesIterable<>(this, input, start, overlapping); + } + /** * Returns a list of all successful non-overlapping parses of the {@code * input}. @@ -105,6 +182,172 @@ public List matchesSkipping(String input) { return (List) list; } + /** + * Returns a lazy iterable of all successful non-overlapping parses of the {@code input} + * starting at {@code start}. + */ + public Iterable matchesSkipping(String input, int start) { + return matches(input, start, false); + } + + /** + * Returns a lazy stream of all successful overlapping parses of the {@code input}. + */ + public Stream matchesAsStream(String input) { + return matchesAsStream(input, 0); + } + + /** + * Returns a lazy stream of all successful overlapping parses of the {@code input} + * starting at {@code start}. + */ + public Stream matchesAsStream(String input, int start) { + return matchesAsStream(input, start, true); + } + + /** + * Returns a lazy stream of all successful parses of the {@code input} + * starting at {@code start}, optionally overlapping. + */ + public Stream matchesAsStream(String input, int start, boolean overlapping) { + return StreamSupport.stream(new MatchesSpliterator<>(this, input, start, overlapping), false); + } + + /** + * Returns a lazy stream of all successful non-overlapping parses of the {@code input}. + */ + public Stream matchesSkippingAsStream(String input) { + return matchesSkippingAsStream(input, 0); + } + + /** + * Returns a lazy stream of all successful non-overlapping parses of the {@code input} + * starting at {@code start}. + */ + public Stream matchesSkippingAsStream(String input, int start) { + return matchesAsStream(input, start, false); + } + + /** + * Returns a parser that reports the current input position. + */ + public static Parser position() { + return PositionParser.INSTANCE; + } + + /** + * Returns a parser that detects newlines platform independently. + */ + public static Parser newline() { + return NewlineParser.INSTANCE; + } + + /** + * Returns a parser that detects newlines platform independently with a custom failure {@code message}. + * + * @param message the failure message. + */ + public static Parser newline(String message) { + return new NewlineParser(message); + } + + /** + * Returns a parser that consumes nothing and succeeds with {@code null}. + */ + public static Parser epsilon() { + return EpsilonParser.INSTANCE; + } + + /** + * Returns a parser that consumes nothing and succeeds with {@code value}. + * + * @param value the result value. + */ + public static Parser epsilon(Object value) { + return new EpsilonParser(value); + } + + /** + * Returns a parser that consumes nothing and fails with a default error message. + */ + public static Parser failure() { + return new FailureParser(); + } + + /** + * Returns a parser that consumes nothing and fails with the specified error {@code message}. + * + * @param message the failure message. + */ + public static Parser failure(String message) { + return FailureParser.withMessage(message); + } + + /** + * Creates a sequence parser of 2 parsers. + */ + public static SequenceParser2 seq( + Parser p1, Parser p2) { + return new SequenceParser2<>(p1, p2); + } + + /** + * Creates a sequence parser of 3 parsers. + */ + public static SequenceParser3 seq( + Parser p1, Parser p2, Parser p3) { + return new SequenceParser3<>(p1, p2, p3); + } + + /** + * Creates a sequence parser of 4 parsers. + */ + public static SequenceParser4 seq( + Parser p1, Parser p2, Parser p3, Parser p4) { + return new SequenceParser4<>(p1, p2, p3, p4); + } + + /** + * Creates a sequence parser of 5 parsers. + */ + public static SequenceParser5 seq( + Parser p1, Parser p2, Parser p3, Parser p4, Parser p5) { + return new SequenceParser5<>(p1, p2, p3, p4, p5); + } + + /** + * Creates a sequence parser of 6 parsers. + */ + public static SequenceParser6 seq( + Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6) { + return new SequenceParser6<>(p1, p2, p3, p4, p5, p6); + } + + /** + * Creates a sequence parser of 7 parsers. + */ + public static SequenceParser7 seq( + Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7) { + return new SequenceParser7<>(p1, p2, p3, p4, p5, p6, p7); + } + + /** + * Creates a sequence parser of 8 parsers. + */ + public static SequenceParser8 seq( + Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7, Parser p8) { + return new SequenceParser8<>(p1, p2, p3, p4, p5, p6, p7, p8); + } + + /** + * Creates a sequence parser of 9 parsers. + */ + public static SequenceParser9 seq( + Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7, Parser p8, Parser p9) { + return new SequenceParser9<>(p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + + /** * Returns new parser that accepts the receiver, if possible. The resulting * parser returns the result of the receiver, or {@code null} if not @@ -230,6 +473,70 @@ public Parser times(int count) { return repeat(count, count); } + /** + * Returns a parser that accepts the receiver zero or more times and returns a + * string of the consumed range. + */ + public Parser starString() { + return repeatString(0, RepeatingParser.UNBOUNDED); + } + + /** + * Returns a parser that accepts the receiver zero or more times and returns a + * string of the consumed range. Reports the provided {@code message} in case of an error. + */ + public Parser starString(String message) { + return repeatString(0, RepeatingParser.UNBOUNDED, message); + } + + /** + * Returns a parser that accepts the receiver one or more times and returns a + * string of the consumed range. + */ + public Parser plusString() { + return repeatString(1, RepeatingParser.UNBOUNDED); + } + + /** + * Returns a parser that accepts the receiver one or more times and returns a + * string of the consumed range. Reports the provided {@code message} in case of an error. + */ + public Parser plusString(String message) { + return repeatString(1, RepeatingParser.UNBOUNDED, message); + } + + /** + * Returns a parser that accepts the receiver exactly {@code count} times and + * returns a string of the consumed range. + */ + public Parser timesString(int count) { + return repeatString(count, count); + } + + /** + * Returns a parser that accepts the receiver exactly {@code count} times and + * returns a string of the consumed range. Reports the provided {@code message} in case of an error. + */ + public Parser timesString(int count, String message) { + return repeatString(count, count, message); + } + + /** + * Returns a parser that accepts the receiver between {@code min} and {@code + * max} times and returns a string of the consumed range. + */ + public Parser repeatString(int min, int max) { + return repeatString(min, max, null); + } + + /** + * Returns a parser that accepts the receiver between {@code min} and {@code + * max} times and returns a string of the consumed range. Reports the provided {@code message} in case of an error. + */ + public Parser repeatString(int min, int max, String message) { + return repeat(min, max).flatten(message); + } + /** * Returns a parser that accepts the receiver followed by {@code others}. The * resulting parser returns a list of the parse result of the receiver @@ -246,6 +553,26 @@ public SequenceParser seq(Parser... others) { return new SequenceParser(parsers); } + /** + * Chains this parser with {@code next}, returning a sequence parser. Automatically + * flattens into {@link SequenceParser3} through {@link SequenceParser9} up to 9 elements. + * + * @param next the next parser in the sequence. + * @param the sequence parser type. + * @return a sequence parser. + */ + @SuppressWarnings("unchecked") + public S then(Parser next) { + return (S) new SequenceParser2<>(this, next); + } + + /** + * Returns a parser that accepts the receiver or {@code other}. + */ + public ChoiceParser or(Parser other) { + return or(new FailureJoiner.SelectLast(), other); + } + /** * Returns a parser that accepts the receiver or {@code others}. The resulting * parser returns the parse result of first succeeding parser (exclusive @@ -268,6 +595,133 @@ public ChoiceParser or(FailureJoiner failureJoiner, Parser... others) { return new ChoiceParser(failureJoiner, parsers); } + /** + * Returns a parser that accepts the receiver or {@code others}, allowing explicit type widening. + */ + public ChoiceParser orWiden(Parser... others) { + return or(others); + } + + /** + * Returns a parser that accepts the receiver or {@code others} using the provided + * {@code failureJoiner}, allowing explicit type widening. + */ + public ChoiceParser orWiden(FailureJoiner failureJoiner, Parser... others) { + return or(failureJoiner, others); + } + + /** + * Creates a choice parser of 2 parsers. + */ + public static ChoiceParser or(Parser p1, Parser p2) { + return new ChoiceParser(p1, p2); + } + + /** + * Creates a choice parser of 3 parsers. + */ + public static ChoiceParser or(Parser p1, Parser p2, Parser p3) { + return new ChoiceParser(p1, p2, p3); + } + + /** + * Creates a choice parser of 4 parsers. + */ + public static ChoiceParser or(Parser p1, Parser p2, Parser p3, Parser p4) { + return new ChoiceParser(p1, p2, p3, p4); + } + + /** + * Creates a choice parser of 5 parsers. + */ + public static ChoiceParser or(Parser p1, Parser p2, Parser p3, Parser p4, Parser p5) { + return new ChoiceParser(p1, p2, p3, p4, p5); + } + + /** + * Creates a choice parser of 6 parsers. + */ + public static ChoiceParser or(Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6) { + return new ChoiceParser(p1, p2, p3, p4, p5, p6); + } + + /** + * Creates a choice parser of 7 parsers. + */ + public static ChoiceParser or(Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7) { + return new ChoiceParser(p1, p2, p3, p4, p5, p6, p7); + } + + /** + * Creates a choice parser of 8 parsers. + */ + public static ChoiceParser or(Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7, Parser p8) { + return new ChoiceParser(p1, p2, p3, p4, p5, p6, p7, p8); + } + + /** + * Creates a choice parser of 9 parsers. + */ + public static ChoiceParser or(Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7, Parser p8, Parser p9) { + return new ChoiceParser(p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + + /** + * Creates a choice parser of 2 parsers using {@code failureJoiner}. + */ + public static ChoiceParser or(FailureJoiner failureJoiner, Parser p1, Parser p2) { + return new ChoiceParser(failureJoiner, p1, p2); + } + + /** + * Creates a choice parser of 3 parsers using {@code failureJoiner}. + */ + public static ChoiceParser or(FailureJoiner failureJoiner, Parser p1, Parser p2, Parser p3) { + return new ChoiceParser(failureJoiner, p1, p2, p3); + } + + /** + * Creates a choice parser of 4 parsers using {@code failureJoiner}. + */ + public static ChoiceParser or(FailureJoiner failureJoiner, Parser p1, Parser p2, Parser p3, Parser p4) { + return new ChoiceParser(failureJoiner, p1, p2, p3, p4); + } + + /** + * Creates a choice parser of 5 parsers using {@code failureJoiner}. + */ + public static ChoiceParser or(FailureJoiner failureJoiner, Parser p1, Parser p2, Parser p3, Parser p4, Parser p5) { + return new ChoiceParser(failureJoiner, p1, p2, p3, p4, p5); + } + + /** + * Creates a choice parser of 6 parsers using {@code failureJoiner}. + */ + public static ChoiceParser or(FailureJoiner failureJoiner, Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6) { + return new ChoiceParser(failureJoiner, p1, p2, p3, p4, p5, p6); + } + + /** + * Creates a choice parser of 7 parsers using {@code failureJoiner}. + */ + public static ChoiceParser or(FailureJoiner failureJoiner, Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7) { + return new ChoiceParser(failureJoiner, p1, p2, p3, p4, p5, p6, p7); + } + + /** + * Creates a choice parser of 8 parsers using {@code failureJoiner}. + */ + public static ChoiceParser or(FailureJoiner failureJoiner, Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7, Parser p8) { + return new ChoiceParser(failureJoiner, p1, p2, p3, p4, p5, p6, p7, p8); + } + + /** + * Creates a choice parser of 9 parsers using {@code failureJoiner}. + */ + public static ChoiceParser or(FailureJoiner failureJoiner, Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7, Parser p8, Parser p9) { + return new ChoiceParser(failureJoiner, p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + /** * Returns a parser (logical and-predicate) that succeeds whenever the * receiver does, but never consumes input. @@ -365,6 +819,19 @@ public Parser trim(Parser before, Parser after) { return new TrimmingParser(this, before, after); } + /** + * Returns a parser that consumes input {@code before} and {@code after} the + * receiver, but discards the parse results of {@code before} and {@code after} + * and only returns the result of the receiver. + * + * @param before the parser to consume before the receiver (or {@code null} for epsilon). + * @param after the parser to consume after the receiver (or {@code null} for epsilon). + * @return a parser that skips input before and after. + */ + public Parser skip(Parser before, Parser after) { + return new SkipParser(this, before, after); + } + /** * Returns a parser that succeeds only if the receiver consumes the complete * input. @@ -389,6 +856,17 @@ public SettableParser settable() { return SettableParser.with(this); } + /** + * Returns a parser that simply defers to its delegate, but that has a {@code label} + * for debugging purposes. + * + * @param label the label of the parser. + * @return a labeled parser. + */ + public Parser labeled(String label) { + return new LabelParser(this, label); + } + /** * Returns a parser that evaluates a {@code function} as the production action * on success of the receiver. @@ -399,6 +877,156 @@ public Parser map(Function function) { return new ActionParser<>(this, function); } + /** + * Returns a parser that evaluates a binary {@code function} on success of the receiver. + */ + @SuppressWarnings("unchecked") + public Parser map(BiFunction function) { + Objects.requireNonNull(function, "Undefined map function"); + return map(input -> { + if (input instanceof Tuple2 tuple) { + return function.apply((T1) tuple.first(), (T2) tuple.second()); + } + List list = (List) input; + return function.apply((T1) list.get(0), (T2) list.get(1)); + }); + } + + /** + * Returns a parser that evaluates a 3-argument {@code function} on success of the receiver. + */ + @SuppressWarnings("unchecked") + public Parser map(Function3 function) { + Objects.requireNonNull(function, "Undefined map function"); + return map(input -> { + if (input instanceof Tuple3 tuple) { + return function.apply( + (T1) tuple.first(), (T2) tuple.second(), (T3) tuple.third()); + } + List list = (List) input; + return function.apply((T1) list.get(0), (T2) list.get(1), (T3) list.get(2)); + }); + } + + /** + * Returns a parser that evaluates a 4-argument {@code function} on success of the receiver. + */ + @SuppressWarnings("unchecked") + public Parser map( + Function4 function) { + Objects.requireNonNull(function, "Undefined map function"); + return map(input -> { + if (input instanceof Tuple4 tuple) { + return function.apply( + (T1) tuple.first(), (T2) tuple.second(), (T3) tuple.third(), (T4) tuple.fourth()); + } + List list = (List) input; + return function.apply((T1) list.get(0), (T2) list.get(1), (T3) list.get(2), (T4) list.get(3)); + }); + } + + /** + * Returns a parser that evaluates a 5-argument {@code function} on success of the receiver. + */ + @SuppressWarnings("unchecked") + public Parser map( + Function5 function) { + Objects.requireNonNull(function, "Undefined map function"); + return map(input -> { + if (input instanceof Tuple5 tuple) { + return function.apply( + (T1) tuple.first(), (T2) tuple.second(), (T3) tuple.third(), (T4) tuple.fourth(), + (T5) tuple.fifth()); + } + List list = (List) input; + return function.apply( + (T1) list.get(0), (T2) list.get(1), (T3) list.get(2), (T4) list.get(3), + (T5) list.get(4)); + }); + } + + /** + * Returns a parser that evaluates a 6-argument {@code function} on success of the receiver. + */ + @SuppressWarnings("unchecked") + public Parser map( + Function6 function) { + Objects.requireNonNull(function, "Undefined map function"); + return map(input -> { + if (input instanceof Tuple6 tuple) { + return function.apply( + (T1) tuple.first(), (T2) tuple.second(), (T3) tuple.third(), (T4) tuple.fourth(), + (T5) tuple.fifth(), (T6) tuple.sixth()); + } + List list = (List) input; + return function.apply( + (T1) list.get(0), (T2) list.get(1), (T3) list.get(2), (T4) list.get(3), + (T5) list.get(4), (T6) list.get(5)); + }); + } + + /** + * Returns a parser that evaluates a 7-argument {@code function} on success of the receiver. + */ + @SuppressWarnings("unchecked") + public Parser map( + Function7 function) { + Objects.requireNonNull(function, "Undefined map function"); + return map(input -> { + if (input instanceof Tuple7 tuple) { + return function.apply( + (T1) tuple.first(), (T2) tuple.second(), (T3) tuple.third(), (T4) tuple.fourth(), + (T5) tuple.fifth(), (T6) tuple.sixth(), (T7) tuple.seventh()); + } + List list = (List) input; + return function.apply( + (T1) list.get(0), (T2) list.get(1), (T3) list.get(2), (T4) list.get(3), + (T5) list.get(4), (T6) list.get(5), (T7) list.get(6)); + }); + } + + /** + * Returns a parser that evaluates a 8-argument {@code function} on success of the receiver. + */ + @SuppressWarnings("unchecked") + public Parser map( + Function8 function) { + Objects.requireNonNull(function, "Undefined map function"); + return map(input -> { + if (input instanceof Tuple8 tuple) { + return function.apply( + (T1) tuple.first(), (T2) tuple.second(), (T3) tuple.third(), (T4) tuple.fourth(), + (T5) tuple.fifth(), (T6) tuple.sixth(), (T7) tuple.seventh(), (T8) tuple.eighth()); + } + List list = (List) input; + return function.apply( + (T1) list.get(0), (T2) list.get(1), (T3) list.get(2), (T4) list.get(3), + (T5) list.get(4), (T6) list.get(5), (T7) list.get(6), (T8) list.get(7)); + }); + } + + /** + * Returns a parser that evaluates a 9-argument {@code function} on success of the receiver. + */ + @SuppressWarnings("unchecked") + public Parser map( + Function9 function) { + Objects.requireNonNull(function, "Undefined map function"); + return map(input -> { + if (input instanceof Tuple9 tuple) { + return function.apply( + (T1) tuple.first(), (T2) tuple.second(), (T3) tuple.third(), (T4) tuple.fourth(), + (T5) tuple.fifth(), (T6) tuple.sixth(), (T7) tuple.seventh(), (T8) tuple.eighth(), + (T9) tuple.ninth()); + } + List list = (List) input; + return function.apply( + (T1) list.get(0), (T2) list.get(1), (T3) list.get(2), (T4) list.get(3), + (T5) list.get(4), (T6) list.get(5), (T7) list.get(6), (T8) list.get(7), + (T9) list.get(8)); + }); + } + /** * Returns a parser that evaluates a {@code function} as the production action * on success of the receiver. @@ -409,13 +1037,81 @@ public Parser mapWithSideEffects(Function function) { return new ActionParser<>(this, function, true); } + /** + * Returns a parser that evaluates the {@code predicate} on the successful + * parse result of the receiver. + * + * @param predicate the predicate to evaluate. + * @param the type of the result to filter. + * @return a filtered parser. + */ + public Parser where(Predicate predicate) { + return where(predicate, (String) null); + } + + /** + * Returns a parser that evaluates the {@code predicate} on the successful + * parse result of the receiver. + * + * @param predicate the predicate to evaluate. + * @param message the failure message if the predicate fails. + * @param the type of the result to filter. + * @return a filtered parser. + */ + public Parser where(Predicate predicate, String message) { + return new WhereParser<>(this, predicate, message); + } + + /** + * Returns a parser that evaluates the {@code predicate} on the successful + * parse result of the receiver. + * + * @param predicate the predicate to evaluate. + * @param failureFactory the failure factory producing a result if the predicate fails. + * @param the type of the result to filter. + * @return a filtered parser. + */ + public Parser where( + Predicate predicate, + BiFunction failureFactory) { + return new WhereParser<>(this, predicate, failureFactory); + } + + /** + * Returns a parser that casts its result to {@code }. + */ + public CastParser cast() { + return new CastParser<>(this); + } + + /** + * Returns a parser that casts its result to {@code clazz}. + */ + public CastParser cast(Class clazz) { + return new CastParser<>(this, clazz); + } + + /** + * Returns a parser that casts its list result to {@code List}. + */ + public CastListParser castList() { + return new CastListParser<>(this); + } + + /** + * Returns a parser that casts its list result to {@code List} using {@code clazz}. + */ + public CastListParser castList(Class clazz) { + return new CastListParser<>(this, clazz); + } + /** * Returns a parser that transform a successful parse result by returning the * element at {@code index} of a list. A negative index can be used to access * the elements from the back of the list. */ - public Parser pick(int index) { - return map(Functions.nthOfList(index)); + public PickParser pick(int index) { + return new PickParser<>(this, index); } /** @@ -423,8 +1119,67 @@ public Parser pick(int index) { * permuted elements at {@code indexes} of a list. Negative indexes can be * used to access the elements from the back of the list. */ - public Parser permute(int... indexes) { - return this.map(Functions.permutationOfList(indexes)); + public PermuteParser permute(int... indexes) { + return new PermuteParser(this, indexes); + } + + /** + * Returns a parser that transforms a successful parse result by returning the + * given constant {@code value}. + * + * @param value the constant value to return. + * @param the type of the constant value. + * @return a parser producing the constant value. + */ + public Parser constant(T value) { + return map(Functions.constant(value)); + } + + /** + * Returns a parser that parses the receiver zero or more times, separated by {@code separator}. + * The resulting parser returns a {@link org.petitparser.parser.repeating.SeparatedList}. + * + * @param separator the separator parser. + * @return a separated repeating parser. + */ + public SeparatedRepeatingParser starSeparated(Parser separator) { + return repeatSeparated(separator, 0, RepeatingParser.UNBOUNDED); + } + + /** + * Returns a parser that parses the receiver one or more times, separated by {@code separator}. + * The resulting parser returns a {@link org.petitparser.parser.repeating.SeparatedList}. + * + * @param separator the separator parser. + * @return a separated repeating parser. + */ + public SeparatedRepeatingParser plusSeparated(Parser separator) { + return repeatSeparated(separator, 1, RepeatingParser.UNBOUNDED); + } + + /** + * Returns a parser that parses the receiver exactly {@code count} times, separated by {@code separator}. + * The resulting parser returns a {@link org.petitparser.parser.repeating.SeparatedList}. + * + * @param separator the separator parser. + * @param count the number of repetitions. + * @return a separated repeating parser. + */ + public SeparatedRepeatingParser timesSeparated(Parser separator, int count) { + return repeatSeparated(separator, count, count); + } + + /** + * Returns a parser that parses the receiver between {@code min} and {@code max} times, + * separated by {@code separator}. The resulting parser returns a {@link org.petitparser.parser.repeating.SeparatedList}. + * + * @param separator the separator parser. + * @param min the minimum number of repetitions. + * @param max the maximum number of repetitions, or {@link RepeatingParser#UNBOUNDED}. + * @return a separated repeating parser. + */ + public SeparatedRepeatingParser repeatSeparated(Parser separator, int min, int max) { + return new SeparatedRepeatingParser(this, separator, min, max); } /** diff --git a/petitparser-core/src/main/java/org/petitparser/parser/actions/ActionParser.java b/petitparser-core/src/main/java/org/petitparser/parser/actions/ActionParser.java index 0bcf829..114d0ea 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/actions/ActionParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/actions/ActionParser.java @@ -32,6 +32,13 @@ public ActionParser( this.hasSideEffects = hasSideEffects; } + /** + * Returns whether this action has side effects. + */ + public boolean hasSideEffects() { + return hasSideEffects; + } + @Override public Result parseOn(Context context) { Result result = delegate.parseOn(context); @@ -45,11 +52,15 @@ public Result parseOn(Context context) { @Override public int fastParseOn(String buffer, int position) { // If we know to have side-effects, we have to fall back to the slow mode. - return hasSideEffects ? super.fastParseOn(buffer, position) : - delegate.fastParseOn(buffer, position); + if (hasSideEffects) { + Result result = parseOn(new Context(buffer, position)); + return result.isSuccess() ? result.getPosition() : -1; + } + return delegate.fastParseOn(buffer, position); } @Override + @SuppressWarnings("unchecked") protected boolean hasEqualProperties(Parser other) { return super.hasEqualProperties(other) && Objects.equals(function, ((ActionParser) other).function) && diff --git a/petitparser-core/src/main/java/org/petitparser/parser/actions/CastListParser.java b/petitparser-core/src/main/java/org/petitparser/parser/actions/CastListParser.java new file mode 100644 index 0000000..0cc191d --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/actions/CastListParser.java @@ -0,0 +1,72 @@ +package org.petitparser.parser.actions; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.DelegateParser; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A parser that casts a successful {@link List} result to a {@code List}. + * + * @param the element type of the resulting list + */ +public class CastListParser extends DelegateParser { + + protected final Class elementClass; + + public CastListParser(Parser delegate) { + this(delegate, null); + } + + public CastListParser(Parser delegate, Class elementClass) { + super(delegate); + this.elementClass = elementClass; + } + + public Class getElementClass() { + return elementClass; + } + + @Override + @SuppressWarnings("unchecked") + public Result parseOn(Context context) { + Result result = delegate.parseOn(context); + if (result.isFailure()) { + return result; + } + List list = result.get(); + if (elementClass != null) { + List typedList = new ArrayList<>(list.size()); + for (Object element : list) { + typedList.add(elementClass.cast(element)); + } + return result.success(typedList); + } + return result.success((List) list); + } + + @Override + public int fastParseOn(String buffer, int position) { + return delegate.fastParseOn(buffer, position); + } + + @Override + public CastListParser copy() { + return new CastListParser<>(delegate, elementClass); + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && + Objects.equals(elementClass, ((CastListParser) other).elementClass); + } + + @Override + public String toString() { + return super.toString() + (elementClass != null ? "[" + elementClass.getSimpleName() + "]" : ""); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/actions/CastParser.java b/petitparser-core/src/main/java/org/petitparser/parser/actions/CastParser.java new file mode 100644 index 0000000..c716b17 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/actions/CastParser.java @@ -0,0 +1,67 @@ +package org.petitparser.parser.actions; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.DelegateParser; + +import java.util.Objects; + +/** + * A parser that casts the result of its delegate to a given type. + * + * @param the input type + * @param the output type + */ +public class CastParser extends DelegateParser { + + protected final Class targetClass; + + public CastParser(Parser delegate) { + this(delegate, null); + } + + public CastParser(Parser delegate, Class targetClass) { + super(delegate); + this.targetClass = targetClass; + } + + public Class getTargetClass() { + return targetClass; + } + + @Override + @SuppressWarnings("unchecked") + public Result parseOn(Context context) { + Result result = delegate.parseOn(context); + if (result.isFailure()) { + return result; + } + Object value = result.get(); + if (targetClass != null) { + return result.success(targetClass.cast(value)); + } + return result.success((S) value); + } + + @Override + public int fastParseOn(String buffer, int position) { + return delegate.fastParseOn(buffer, position); + } + + @Override + public CastParser copy() { + return new CastParser<>(delegate, targetClass); + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && + Objects.equals(targetClass, ((CastParser) other).targetClass); + } + + @Override + public String toString() { + return super.toString() + (targetClass != null ? "[" + targetClass.getSimpleName() + "]" : ""); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/actions/ContinuationParser.java b/petitparser-core/src/main/java/org/petitparser/parser/actions/ContinuationParser.java index 69a8d33..fe6ac28 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/actions/ContinuationParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/actions/ContinuationParser.java @@ -31,6 +31,12 @@ public Result parseOn(Context context) { return handler.apply(super::parseOn, context); } + @Override + public int fastParseOn(String buffer, int position) { + Result result = parseOn(new Context(buffer, position)); + return result.isSuccess() ? result.getPosition() : -1; + } + @Override protected boolean hasEqualProperties(Parser other) { return super.hasEqualProperties(other) && diff --git a/petitparser-core/src/main/java/org/petitparser/parser/actions/FlattenParser.java b/petitparser-core/src/main/java/org/petitparser/parser/actions/FlattenParser.java index 9bce7c4..8ca2824 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/actions/FlattenParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/actions/FlattenParser.java @@ -45,6 +45,19 @@ public Result parseOn(Context context) { } } + /** + * Returns the failure message used for fast parsing, or null. + */ + public String getMessage() { + return message; + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && + java.util.Objects.equals(message, ((FlattenParser) other).message); + } + @Override public FlattenParser copy() { return new FlattenParser(delegate, message); diff --git a/petitparser-core/src/main/java/org/petitparser/parser/actions/PermuteParser.java b/petitparser-core/src/main/java/org/petitparser/parser/actions/PermuteParser.java new file mode 100644 index 0000000..44cdb9f --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/actions/PermuteParser.java @@ -0,0 +1,79 @@ +package org.petitparser.parser.actions; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.DelegateParser; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * A parser that transforms a successful parse result by returning the + * permuted elements at {@code indexes} of a list. Negative indexes can be + * used to access the elements from the back of the list. + */ +public class PermuteParser extends DelegateParser { + + protected final int[] indexes; + + public PermuteParser(Parser delegate, int... indexes) { + super(delegate); + this.indexes = Objects.requireNonNull(indexes, "Undefined indexes").clone(); + } + + /** + * Returns a copy of the permutation indices. + */ + public int[] getIndices() { + return indexes.clone(); + } + + + @Override + public Result parseOn(Context context) { + Result result = delegate.parseOn(context); + if (result.isFailure()) { + return result; + } + List list = result.get(); + int size = list.size(); + Object permuted = permuteList(list, size); + return result.success(permuted); + } + + private Object permuteList(List list, int size) { + if (indexes.length == 0) { + return Collections.emptyList(); + } else { + List result = new ArrayList<>(indexes.length); + for (int index : indexes) { + result.add(list.get(index < 0 ? size + index : index)); + } + return result; + } + } + + @Override + public int fastParseOn(String buffer, int position) { + return delegate.fastParseOn(buffer, position); + } + + @Override + public PermuteParser copy() { + return new PermuteParser(delegate, indexes); + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && + Arrays.equals(indexes, ((PermuteParser) other).indexes); + } + + @Override + public String toString() { + return super.toString() + Arrays.toString(indexes); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/actions/PickParser.java b/petitparser-core/src/main/java/org/petitparser/parser/actions/PickParser.java new file mode 100644 index 0000000..9a082a6 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/actions/PickParser.java @@ -0,0 +1,62 @@ +package org.petitparser.parser.actions; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.DelegateParser; + +import java.util.List; + +/** + * A parser that transforms a successful parse result by returning the element at + * {@code index} of a list. A negative index can be used to access the elements + * from the back of the list. + * + * @param the picked element type + */ +public class PickParser extends DelegateParser { + + protected final int index; + + public PickParser(Parser delegate, int index) { + super(delegate); + this.index = index; + } + + public int getIndex() { + return index; + } + + @Override + @SuppressWarnings("unchecked") + public Result parseOn(Context context) { + Result result = delegate.parseOn(context); + if (result.isFailure()) { + return result; + } + List list = result.get(); + int size = list.size(); + int actualIndex = index < 0 ? size + index : index; + return result.success((T) list.get(actualIndex)); + } + + @Override + public int fastParseOn(String buffer, int position) { + return delegate.fastParseOn(buffer, position); + } + + @Override + public PickParser copy() { + return new PickParser<>(delegate, index); + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && index == ((PickParser) other).index; + } + + @Override + public String toString() { + return super.toString() + "[" + index + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/actions/TrimmingParser.java b/petitparser-core/src/main/java/org/petitparser/parser/actions/TrimmingParser.java index 805da80..30ed064 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/actions/TrimmingParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/actions/TrimmingParser.java @@ -4,6 +4,7 @@ import org.petitparser.context.Result; import org.petitparser.parser.Parser; import org.petitparser.parser.combinators.DelegateParser; +import org.petitparser.parser.combinators.SequentialParser; import java.util.Arrays; import java.util.List; @@ -12,7 +13,7 @@ /** * A parser that silently consumes a before and after the delegate parser. */ -public class TrimmingParser extends DelegateParser { +public class TrimmingParser extends DelegateParser implements SequentialParser { private Parser left; private Parser right; diff --git a/petitparser-core/src/main/java/org/petitparser/parser/actions/WhereParser.java b/petitparser-core/src/main/java/org/petitparser/parser/actions/WhereParser.java new file mode 100644 index 0000000..586a857 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/actions/WhereParser.java @@ -0,0 +1,131 @@ +package org.petitparser.parser.actions; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.DelegateParser; + +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +/** + * A parser that evaluates a predicate on the successful result of its delegate. + * + * @param The type of the result to filter. + */ +public class WhereParser extends DelegateParser { + + protected final Predicate predicate; + protected final String message; + protected final BiFunction failureFactory; + + /** + * Constructs a where parser with a default failure message. + * + * @param delegate the delegate parser. + * @param predicate the predicate to evaluate on parse success. + */ + public WhereParser(Parser delegate, Predicate predicate) { + this(delegate, predicate, null, null); + } + + /** + * Constructs a where parser with a custom failure message. + * + * @param delegate the delegate parser. + * @param predicate the predicate to evaluate on parse success. + * @param message the failure message if the predicate returns {@code false}. + */ + public WhereParser(Parser delegate, Predicate predicate, String message) { + this(delegate, predicate, message, null); + } + + /** + * Constructs a where parser with a custom failure factory. + * + * @param delegate the delegate parser. + * @param predicate the predicate to evaluate on parse success. + * @param failureFactory the factory function to create a failure result. + */ + public WhereParser( + Parser delegate, + Predicate predicate, + BiFunction failureFactory) { + this(delegate, predicate, null, + Objects.requireNonNull(failureFactory, "Undefined failure factory")); + } + + protected WhereParser( + Parser delegate, + Predicate predicate, + String message, + BiFunction failureFactory) { + super(delegate); + this.predicate = Objects.requireNonNull(predicate, "Undefined predicate"); + this.message = message; + this.failureFactory = failureFactory; + } + + /** + * Returns the predicate used to filter parse results. + */ + public Predicate getPredicate() { + return predicate; + } + + /** + * Returns the failure message if configured, or {@code null}. + */ + public String getMessage() { + return message; + } + + /** + * Returns the failure factory if configured, or {@code null}. + */ + public BiFunction getFailureFactory() { + return failureFactory; + } + + @Override + @SuppressWarnings("unchecked") + public Result parseOn(Context context) { + Result result = delegate.parseOn(context); + if (result.isSuccess() && !predicate.test((T) result.get())) { + if (failureFactory != null) { + return failureFactory.apply(context, result); + } + return context.failure(message != null ? message : "unexpected \"" + result.get() + "\""); + } + return result; + } + + @Override + @SuppressWarnings("unchecked") + public int fastParseOn(String buffer, int position) { + Result result = delegate.parseOn(new Context(buffer, position)); + return result.isSuccess() && predicate.test((T) result.get()) + ? result.getPosition() + : -1; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && + Objects.equals(predicate, ((WhereParser) other).predicate) && + Objects.equals(message, ((WhereParser) other).message) && + Objects.equals(failureFactory, ((WhereParser) other).failureFactory); + } + + @Override + public String toString() { + return super.toString() + (message != null ? "[" + message + "]" : ""); + } + + @Override + public WhereParser copy() { + return new WhereParser<>(delegate, predicate, message, failureFactory); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/ChoiceParser.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/ChoiceParser.java index f7dd8e2..3633363 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/combinators/ChoiceParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/ChoiceParser.java @@ -54,6 +54,11 @@ public int fastParseOn(String buffer, int position) { return result; } + @Override + public ChoiceParser or(Parser other) { + return or(failureJoiner, other); + } + @Override public ChoiceParser or(FailureJoiner failureJoiner, Parser... others) { Parser[] array = Arrays.copyOf(parsers, parsers.length + others.length); @@ -61,6 +66,21 @@ public ChoiceParser or(FailureJoiner failureJoiner, Parser... others) { return new ChoiceParser(failureJoiner, array); } + /** + * Returns the failure joiner used by this choice parser. + * + * @return the failure joiner + */ + public FailureJoiner getFailureJoiner() { + return failureJoiner; + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && + java.util.Objects.equals(failureJoiner, ((ChoiceParser) other).failureJoiner); + } + @Override public ChoiceParser copy() { return new ChoiceParser(failureJoiner, Arrays.copyOf(parsers, diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/DelegateParser.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/DelegateParser.java index 4a1e610..c57f30a 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/combinators/DelegateParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/DelegateParser.java @@ -25,6 +25,11 @@ public Result parseOn(Context context) { return delegate.parseOn(context); } + @Override + public int fastParseOn(String buffer, int position) { + return delegate.fastParseOn(buffer, position); + } + @Override public void replace(Parser source, Parser target) { super.replace(source, target); diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/LabelParser.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/LabelParser.java new file mode 100644 index 0000000..fc9615e --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/LabelParser.java @@ -0,0 +1,59 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; + +import java.util.Objects; + +/** + * A parser that always defers to its delegate, but that also holds a label for debugging purposes. + */ +public class LabelParser extends DelegateParser { + + protected final String label; + + /** + * Constructs a label parser wrapping the {@code delegate} with a {@code label}. + * + * @param delegate the delegate parser. + * @param label the label of the parser. + */ + public LabelParser(Parser delegate, String label) { + super(delegate); + this.label = Objects.requireNonNull(label, "Undefined label"); + } + + /** + * Returns the label of this parser. + */ + public String getLabel() { + return label; + } + + @Override + public Result parseOn(Context context) { + return delegate.parseOn(context); + } + + @Override + public int fastParseOn(String buffer, int position) { + return delegate.fastParseOn(buffer, position); + } + + @Override + public String toString() { + return delegate.toString() + "[" + label + "]"; + } + + @Override + public LabelParser copy() { + return new LabelParser(delegate, label); + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && + Objects.equals(label, ((LabelParser) other).label); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/ResolvableParser.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/ResolvableParser.java new file mode 100644 index 0000000..4eb493d --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/ResolvableParser.java @@ -0,0 +1,16 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.parser.Parser; + +/** + * Interface of a parser that can be resolved to another one. + */ +public interface ResolvableParser { + + /** + * Resolves this parser to another one. + * + * @return the resolved parser + */ + Parser resolve(); +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser.java index 7199920..695e6ea 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser.java @@ -11,7 +11,7 @@ /** * A parser that parses a sequence of parsers. */ -public class SequenceParser extends ListParser { +public class SequenceParser extends ListParser implements SequentialParser { public SequenceParser(Parser... parsers) { super(parsers); diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser2.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser2.java new file mode 100644 index 0000000..711386a --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser2.java @@ -0,0 +1,54 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.utils.tuples.Tuple2; + +/** + * A parser that parses a sequence of 2 parsers and produces a {@link Tuple2}. + * + * @param the result type of the first parser + * @param the result type of the second parser + */ +public class SequenceParser2 extends ListParser implements SequentialParser { + + public SequenceParser2(Parser p1, Parser p2) { + super(p1, p2); + } + + @Override + public Result parseOn(Context context) { + Result r1 = parsers[0].parseOn(context); + if (r1.isFailure()) { + return r1; + } + Result r2 = parsers[1].parseOn(r1); + if (r2.isFailure()) { + return r2; + } + return r2.success(new Tuple2<>(r1.get(), r2.get())); + } + + @Override + public int fastParseOn(String buffer, int position) { + position = parsers[0].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + return parsers[1].fastParseOn(buffer, position); + } + + + + @Override + @SuppressWarnings("unchecked") + public S then(Parser next) { + return (S) new SequenceParser3<>(parsers[0], parsers[1], next); + } + + @Override + public SequenceParser2 copy() { + return new SequenceParser2<>(parsers[0], parsers[1]); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser3.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser3.java new file mode 100644 index 0000000..26d4f43 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser3.java @@ -0,0 +1,63 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.utils.tuples.Tuple3; + +/** + * A parser that parses a sequence of 3 parsers and produces a {@link Tuple3}. + * + * @param the result type of the first parser + * @param the result type of the second parser + * @param the result type of the third parser + */ +public class SequenceParser3 extends ListParser implements SequentialParser { + + public SequenceParser3(Parser p1, Parser p2, Parser p3) { + super(p1, p2, p3); + } + + @Override + public Result parseOn(Context context) { + Result r1 = parsers[0].parseOn(context); + if (r1.isFailure()) { + return r1; + } + Result r2 = parsers[1].parseOn(r1); + if (r2.isFailure()) { + return r2; + } + Result r3 = parsers[2].parseOn(r2); + if (r3.isFailure()) { + return r3; + } + return r3.success(new Tuple3<>(r1.get(), r2.get(), r3.get())); + } + + @Override + public int fastParseOn(String buffer, int position) { + position = parsers[0].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[1].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + return parsers[2].fastParseOn(buffer, position); + } + + + + @Override + @SuppressWarnings("unchecked") + public S then(Parser next) { + return (S) new SequenceParser4<>(parsers[0], parsers[1], parsers[2], next); + } + + @Override + public SequenceParser3 copy() { + return new SequenceParser3<>(parsers[0], parsers[1], parsers[2]); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser4.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser4.java new file mode 100644 index 0000000..85963b9 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser4.java @@ -0,0 +1,72 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.utils.tuples.Tuple4; + +/** + * A parser that parses a sequence of 4 parsers and produces a {@link Tuple4}. + * + * @param the result type of the first parser + * @param the result type of the second parser + * @param the result type of the third parser + * @param the result type of the fourth parser + */ +public class SequenceParser4 extends ListParser implements SequentialParser { + + public SequenceParser4(Parser p1, Parser p2, Parser p3, Parser p4) { + super(p1, p2, p3, p4); + } + + @Override + public Result parseOn(Context context) { + Result r1 = parsers[0].parseOn(context); + if (r1.isFailure()) { + return r1; + } + Result r2 = parsers[1].parseOn(r1); + if (r2.isFailure()) { + return r2; + } + Result r3 = parsers[2].parseOn(r2); + if (r3.isFailure()) { + return r3; + } + Result r4 = parsers[3].parseOn(r3); + if (r4.isFailure()) { + return r4; + } + return r4.success(new Tuple4<>(r1.get(), r2.get(), r3.get(), r4.get())); + } + + @Override + public int fastParseOn(String buffer, int position) { + position = parsers[0].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[1].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[2].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + return parsers[3].fastParseOn(buffer, position); + } + + + + @Override + @SuppressWarnings("unchecked") + public S then(Parser next) { + return (S) new SequenceParser5<>(parsers[0], parsers[1], parsers[2], parsers[3], next); + } + + @Override + public SequenceParser4 copy() { + return new SequenceParser4<>(parsers[0], parsers[1], parsers[2], parsers[3]); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser5.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser5.java new file mode 100644 index 0000000..7aee980 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser5.java @@ -0,0 +1,82 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.utils.tuples.Tuple5; + +/** + * A parser that parses a sequence of 5 parsers and produces a {@link Tuple5}. + * + * @param the result type of the first parser + * @param the result type of the second parser + * @param the result type of the third parser + * @param the result type of the fourth parser + * @param the result type of the fifth parser + */ +public class SequenceParser5 extends ListParser implements SequentialParser { + + public SequenceParser5(Parser p1, Parser p2, Parser p3, Parser p4, Parser p5) { + super(p1, p2, p3, p4, p5); + } + + @Override + public Result parseOn(Context context) { + Result r1 = parsers[0].parseOn(context); + if (r1.isFailure()) { + return r1; + } + Result r2 = parsers[1].parseOn(r1); + if (r2.isFailure()) { + return r2; + } + Result r3 = parsers[2].parseOn(r2); + if (r3.isFailure()) { + return r3; + } + Result r4 = parsers[3].parseOn(r3); + if (r4.isFailure()) { + return r4; + } + Result r5 = parsers[4].parseOn(r4); + if (r5.isFailure()) { + return r5; + } + return r5.success(new Tuple5<>(r1.get(), r2.get(), r3.get(), r4.get(), r5.get())); + } + + @Override + public int fastParseOn(String buffer, int position) { + position = parsers[0].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[1].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[2].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[3].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + return parsers[4].fastParseOn(buffer, position); + } + + + + @Override + @SuppressWarnings("unchecked") + public S then(Parser next) { + return (S) new SequenceParser6<>( + parsers[0], parsers[1], parsers[2], parsers[3], parsers[4], next); + } + + @Override + public SequenceParser5 copy() { + return new SequenceParser5<>(parsers[0], parsers[1], parsers[2], parsers[3], parsers[4]); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser6.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser6.java new file mode 100644 index 0000000..fb61752 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser6.java @@ -0,0 +1,94 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.utils.tuples.Tuple6; + +/** + * A parser that parses a sequence of 6 parsers and produces a {@link Tuple6}. + * + * @param the result type of the first parser + * @param the result type of the second parser + * @param the result type of the third parser + * @param the result type of the fourth parser + * @param the result type of the fifth parser + * @param the result type of the sixth parser + */ +public class SequenceParser6 extends ListParser implements SequentialParser { + + public SequenceParser6( + Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6) { + super(p1, p2, p3, p4, p5, p6); + } + + @Override + public Result parseOn(Context context) { + Result r1 = parsers[0].parseOn(context); + if (r1.isFailure()) { + return r1; + } + Result r2 = parsers[1].parseOn(r1); + if (r2.isFailure()) { + return r2; + } + Result r3 = parsers[2].parseOn(r2); + if (r3.isFailure()) { + return r3; + } + Result r4 = parsers[3].parseOn(r3); + if (r4.isFailure()) { + return r4; + } + Result r5 = parsers[4].parseOn(r4); + if (r5.isFailure()) { + return r5; + } + Result r6 = parsers[5].parseOn(r5); + if (r6.isFailure()) { + return r6; + } + return r6.success(new Tuple6<>( + r1.get(), r2.get(), r3.get(), r4.get(), r5.get(), r6.get())); + } + + @Override + public int fastParseOn(String buffer, int position) { + position = parsers[0].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[1].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[2].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[3].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[4].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + return parsers[5].fastParseOn(buffer, position); + } + + + + @Override + @SuppressWarnings("unchecked") + public S then(Parser next) { + return (S) new SequenceParser7<>( + parsers[0], parsers[1], parsers[2], parsers[3], parsers[4], parsers[5], next); + } + + @Override + public SequenceParser6 copy() { + return new SequenceParser6<>( + parsers[0], parsers[1], parsers[2], parsers[3], parsers[4], parsers[5]); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser7.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser7.java new file mode 100644 index 0000000..8872c06 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser7.java @@ -0,0 +1,104 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.utils.tuples.Tuple7; + +/** + * A parser that parses a sequence of 7 parsers and produces a {@link Tuple7}. + * + * @param the result type of the first parser + * @param the result type of the second parser + * @param the result type of the third parser + * @param the result type of the fourth parser + * @param the result type of the fifth parser + * @param the result type of the sixth parser + * @param the result type of the seventh parser + */ +public class SequenceParser7 extends ListParser implements SequentialParser { + + public SequenceParser7( + Parser p1, Parser p2, Parser p3, Parser p4, Parser p5, Parser p6, Parser p7) { + super(p1, p2, p3, p4, p5, p6, p7); + } + + @Override + public Result parseOn(Context context) { + Result r1 = parsers[0].parseOn(context); + if (r1.isFailure()) { + return r1; + } + Result r2 = parsers[1].parseOn(r1); + if (r2.isFailure()) { + return r2; + } + Result r3 = parsers[2].parseOn(r2); + if (r3.isFailure()) { + return r3; + } + Result r4 = parsers[3].parseOn(r3); + if (r4.isFailure()) { + return r4; + } + Result r5 = parsers[4].parseOn(r4); + if (r5.isFailure()) { + return r5; + } + Result r6 = parsers[5].parseOn(r5); + if (r6.isFailure()) { + return r6; + } + Result r7 = parsers[6].parseOn(r6); + if (r7.isFailure()) { + return r7; + } + return r7.success(new Tuple7<>( + r1.get(), r2.get(), r3.get(), r4.get(), r5.get(), r6.get(), r7.get())); + } + + @Override + public int fastParseOn(String buffer, int position) { + position = parsers[0].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[1].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[2].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[3].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[4].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[5].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + return parsers[6].fastParseOn(buffer, position); + } + + + + @Override + @SuppressWarnings("unchecked") + public S then(Parser next) { + return (S) new SequenceParser8<>( + parsers[0], parsers[1], parsers[2], parsers[3], parsers[4], parsers[5], + parsers[6], next); + } + + @Override + public SequenceParser7 copy() { + return new SequenceParser7<>( + parsers[0], parsers[1], parsers[2], parsers[3], parsers[4], parsers[5], parsers[6]); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser8.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser8.java new file mode 100644 index 0000000..9648642 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser8.java @@ -0,0 +1,116 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.utils.tuples.Tuple8; + +/** + * A parser that parses a sequence of 8 parsers and produces a {@link Tuple8}. + * + * @param the result type of the first parser + * @param the result type of the second parser + * @param the result type of the third parser + * @param the result type of the fourth parser + * @param the result type of the fifth parser + * @param the result type of the sixth parser + * @param the result type of the seventh parser + * @param the result type of the eighth parser + */ +public class SequenceParser8 extends ListParser implements SequentialParser { + + public SequenceParser8( + Parser p1, Parser p2, Parser p3, Parser p4, + Parser p5, Parser p6, Parser p7, Parser p8) { + super(p1, p2, p3, p4, p5, p6, p7, p8); + } + + @Override + public Result parseOn(Context context) { + Result r1 = parsers[0].parseOn(context); + if (r1.isFailure()) { + return r1; + } + Result r2 = parsers[1].parseOn(r1); + if (r2.isFailure()) { + return r2; + } + Result r3 = parsers[2].parseOn(r2); + if (r3.isFailure()) { + return r3; + } + Result r4 = parsers[3].parseOn(r3); + if (r4.isFailure()) { + return r4; + } + Result r5 = parsers[4].parseOn(r4); + if (r5.isFailure()) { + return r5; + } + Result r6 = parsers[5].parseOn(r5); + if (r6.isFailure()) { + return r6; + } + Result r7 = parsers[6].parseOn(r6); + if (r7.isFailure()) { + return r7; + } + Result r8 = parsers[7].parseOn(r7); + if (r8.isFailure()) { + return r8; + } + return r8.success(new Tuple8<>( + r1.get(), r2.get(), r3.get(), r4.get(), + r5.get(), r6.get(), r7.get(), r8.get())); + } + + @Override + public int fastParseOn(String buffer, int position) { + position = parsers[0].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[1].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[2].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[3].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[4].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[5].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[6].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + return parsers[7].fastParseOn(buffer, position); + } + + + + @Override + @SuppressWarnings("unchecked") + public S then(Parser next) { + return (S) new SequenceParser9<>( + parsers[0], parsers[1], parsers[2], parsers[3], + parsers[4], parsers[5], parsers[6], parsers[7], next); + } + + @Override + public SequenceParser8 copy() { + return new SequenceParser8<>( + parsers[0], parsers[1], parsers[2], parsers[3], + parsers[4], parsers[5], parsers[6], parsers[7]); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser9.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser9.java new file mode 100644 index 0000000..8de3413 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequenceParser9.java @@ -0,0 +1,127 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.utils.tuples.Tuple9; + +/** + * A parser that parses a sequence of 9 parsers and produces a {@link Tuple9}. + * + * @param the result type of the 1st parser + * @param the result type of the 2nd parser + * @param the result type of the 3rd parser + * @param the result type of the 4th parser + * @param the result type of the 5th parser + * @param the result type of the 6th parser + * @param the result type of the 7th parser + * @param the result type of the 8th parser + * @param the result type of the 9th parser + */ +public class SequenceParser9 extends ListParser implements SequentialParser { + + public SequenceParser9( + Parser p1, Parser p2, Parser p3, Parser p4, + Parser p5, Parser p6, Parser p7, Parser p8, Parser p9) { + super(p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + + @Override + public Result parseOn(Context context) { + Result r1 = parsers[0].parseOn(context); + if (r1.isFailure()) { + return r1; + } + Result r2 = parsers[1].parseOn(r1); + if (r2.isFailure()) { + return r2; + } + Result r3 = parsers[2].parseOn(r2); + if (r3.isFailure()) { + return r3; + } + Result r4 = parsers[3].parseOn(r3); + if (r4.isFailure()) { + return r4; + } + Result r5 = parsers[4].parseOn(r4); + if (r5.isFailure()) { + return r5; + } + Result r6 = parsers[5].parseOn(r5); + if (r6.isFailure()) { + return r6; + } + Result r7 = parsers[6].parseOn(r6); + if (r7.isFailure()) { + return r7; + } + Result r8 = parsers[7].parseOn(r7); + if (r8.isFailure()) { + return r8; + } + Result r9 = parsers[8].parseOn(r8); + if (r9.isFailure()) { + return r9; + } + return r9.success(new Tuple9<>( + r1.get(), r2.get(), r3.get(), r4.get(), + r5.get(), r6.get(), r7.get(), r8.get(), r9.get())); + } + + @Override + public int fastParseOn(String buffer, int position) { + position = parsers[0].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[1].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[2].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[3].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[4].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[5].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[6].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + position = parsers[7].fastParseOn(buffer, position); + if (position < 0) { + return position; + } + return parsers[8].fastParseOn(buffer, position); + } + + + + + @Override + @SuppressWarnings("unchecked") + public S then(Parser next) { + Parser[] array = new Parser[parsers.length + 1]; + System.arraycopy(parsers, 0, array, 0, parsers.length); + array[parsers.length] = next; + return (S) new SequenceParser(array); + } + + @Override + public SequenceParser9 copy() { + return new SequenceParser9<>( + parsers[0], parsers[1], parsers[2], parsers[3], + parsers[4], parsers[5], parsers[6], parsers[7], parsers[8]); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequentialParser.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequentialParser.java new file mode 100644 index 0000000..e3be1dc --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SequentialParser.java @@ -0,0 +1,7 @@ +package org.petitparser.parser.combinators; + +/** + * Marker interface of a parser that consumes its children sequentially. + */ +public interface SequentialParser { +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SettableParser.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SettableParser.java index fe4a049..f95b675 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SettableParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SettableParser.java @@ -6,7 +6,7 @@ /** * A parser that can be set to behave like another parser. */ -public class SettableParser extends DelegateParser { +public class SettableParser extends DelegateParser implements ResolvableParser { /** * Constructs a {@link SettableParser} that currently refers to an {@link @@ -48,6 +48,11 @@ public Parser get() { return delegate; } + @Override + public Parser resolve() { + return delegate; + } + /** * Replace the current referred parser with a new {@code delegate}. */ diff --git a/petitparser-core/src/main/java/org/petitparser/parser/combinators/SkipParser.java b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SkipParser.java new file mode 100644 index 0000000..4ce64f4 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/combinators/SkipParser.java @@ -0,0 +1,105 @@ +package org.petitparser.parser.combinators; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.EpsilonParser; + +import java.util.Arrays; +import java.util.List; + +/** + * A parser that silently consumes input of another parser before and after its delegate. + */ +public class SkipParser extends DelegateParser implements SequentialParser { + + protected Parser before; + protected Parser after; + + /** + * Constructs a skip parser that delegates to {@code delegate} with empty before and after parsers. + * + * @param delegate the delegate parser. + */ + public SkipParser(Parser delegate) { + this(delegate, null, null); + } + + /** + * Constructs a skip parser with the specified before and after parsers. + * + * @param delegate the delegate parser. + * @param before the parser to consume before the delegate (or {@code null} for epsilon). + * @param after the parser to consume after the delegate (or {@code null} for epsilon). + */ + public SkipParser(Parser delegate, Parser before, Parser after) { + super(delegate); + this.before = before != null ? before : EpsilonParser.INSTANCE; + this.after = after != null ? after : EpsilonParser.INSTANCE; + } + + /** + * Returns the parser executed before the delegate. + */ + public Parser getBefore() { + return before; + } + + /** + * Returns the parser executed after the delegate. + */ + public Parser getAfter() { + return after; + } + + @Override + public Result parseOn(Context context) { + Result beforeResult = before.parseOn(context); + if (beforeResult.isFailure()) { + return beforeResult; + } + Result result = delegate.parseOn(beforeResult); + if (result.isFailure()) { + return result; + } + Result afterResult = after.parseOn(result); + if (afterResult.isFailure()) { + return afterResult; + } + return afterResult.success(result.get()); + } + + @Override + public int fastParseOn(String buffer, int position) { + position = before.fastParseOn(buffer, position); + if (position < 0) { + return -1; + } + position = delegate.fastParseOn(buffer, position); + if (position < 0) { + return -1; + } + return after.fastParseOn(buffer, position); + } + + @Override + public List getChildren() { + return Arrays.asList(before, delegate, after); + } + + @Override + public void replace(Parser source, Parser target) { + super.replace(source, target); + if (before == source) { + before = target; + } + if (after == source) { + after = target; + } + } + + @Override + public SkipParser copy() { + return new SkipParser(delegate, before, after); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterParser.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterParser.java index 1db1021..cc19954 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterParser.java @@ -3,7 +3,10 @@ import org.petitparser.context.Context; import org.petitparser.context.Result; import org.petitparser.parser.Parser; +import org.petitparser.parser.repeating.RepeatingCharacterParser; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -16,64 +19,116 @@ public class CharacterParser extends Parser { */ public static CharacterParser of( CharacterPredicate predicate, String message) { - return new CharacterParser(predicate, message); + return of(predicate, message, false); + } + + public static CharacterParser of( + CharacterPredicate predicate, String message, boolean unicode) { + return unicode ? new UnicodeCharacterParser(predicate, message) + : new CharacterParser(predicate, message); } /** * Returns a parser that accepts a specific {@code character}. */ public static CharacterParser of(char character) { - return of(character, "'" + toReadableString(character) + "' expected"); + return of(character, false); + } + + public static CharacterParser of(char character, boolean unicode) { + return of(character, "'" + toReadableString(character) + "' expected", unicode); } public static CharacterParser of(char character, String message) { - return of(CharacterPredicate.of(character), message); + return of(character, message, false); + } + + public static CharacterParser of(char character, String message, boolean unicode) { + return of(new SingleCharPredicate(character), message, unicode); + } + + public static CharacterParser of(int codePoint, boolean unicode) { + return of(codePoint, "'" + toReadableString(codePoint) + "' expected", unicode); + } + + public static CharacterParser of(int codePoint, String message, boolean unicode) { + return of(new SingleCharPredicate(codePoint), message, unicode); } /** * Returns a parser that accepts any character. */ public static CharacterParser any() { - return any("any character expected"); + return any(false); } public static CharacterParser any(String message) { - return of(CharacterPredicate.any(), message); + return any(message, false); + } + + public static CharacterParser any(boolean unicode) { + return any("any character expected", unicode); + } + + public static CharacterParser any(String message, boolean unicode) { + return of(ConstantCharPredicate.any(), message, unicode); } /** * Returns a parser that accepts any of the provided characters. */ public static CharacterParser anyOf(String characters) { - return anyOf(characters, "any of '" + toReadableString(characters) + "' " + - "expected"); + return anyOf(characters, false); + } + + public static CharacterParser anyOf(String characters, boolean unicode) { + return anyOf(characters, "any of '" + toReadableString(characters) + "' expected", unicode); } public static CharacterParser anyOf(String chars, String message) { - return of(CharacterPredicate.anyOf(chars), message); + return anyOf(chars, message, false); + } + + public static CharacterParser anyOf(String chars, String message, boolean unicode) { + return of(CharacterPredicate.optimizedString(chars, false, unicode), message, unicode); } /** * Returns a parser that accepts no character. */ public static CharacterParser none() { - return none("no character expected"); + return none(false); } public static CharacterParser none(String message) { - return of(CharacterPredicate.none(), message); + return none(message, false); + } + + public static CharacterParser none(boolean unicode) { + return none("no character expected", unicode); + } + + public static CharacterParser none(String message, boolean unicode) { + return of(ConstantCharPredicate.none(), message, unicode); } /** * Returns a parser that accepts none of the provided characters. */ public static CharacterParser noneOf(String characters) { - return noneOf(characters, "none of '" + toReadableString(characters) + "'" + - " expected"); + return noneOf(characters, false); + } + + public static CharacterParser noneOf(String characters, boolean unicode) { + return noneOf(characters, "none of '" + toReadableString(characters) + "' expected", unicode); } public static CharacterParser noneOf(String chars, String message) { - return of(CharacterPredicate.noneOf(chars), message); + return noneOf(chars, message, false); + } + + public static CharacterParser noneOf(String chars, String message, boolean unicode) { + return of(CharacterPredicate.optimizedString(chars, false, unicode).not(), message, unicode); } /** @@ -84,7 +139,7 @@ public static CharacterParser digit() { } public static CharacterParser digit(String message) { - return new CharacterParser(Character::isDigit, message); + return of(DigitCharPredicate.INSTANCE, message); } /** @@ -95,7 +150,7 @@ public static CharacterParser letter() { } public static CharacterParser letter(String message) { - return of(Character::isLetter, message); + return of(LetterCharPredicate.INSTANCE, message); } /** @@ -106,7 +161,7 @@ public static CharacterParser lowerCase() { } public static CharacterParser lowerCase(String message) { - return of(Character::isLowerCase, message); + return of(LowercaseCharPredicate.INSTANCE, message); } /** @@ -117,23 +172,135 @@ public static CharacterParser lowerCase(String message) { * beginning negates the pattern. */ public static CharacterParser pattern(String pattern) { - return pattern(pattern, "[" + toReadableString(pattern) + "] expected"); + return pattern(pattern, false); } public static CharacterParser pattern(String pattern, String message) { - return of(CharacterPredicate.pattern(pattern), message); + return pattern(pattern, message, false, false); + } + + public static CharacterParser pattern(String pattern, boolean unicode) { + return pattern(pattern, null, false, unicode); + } + + public static CharacterParser pattern(String pattern, String message, boolean unicode) { + return pattern(pattern, message, false, unicode); + } + + @SuppressWarnings("unchecked") + public static CharacterParser pattern( + String pattern, String message, boolean ignoreCase, boolean unicode) { + String input = pattern; + boolean isNegated = input.startsWith("^"); + if (isNegated) { + input = input.substring(1); + } + Parser parser = unicode + ? PatternGrammar.PATTERN_UNICODE_PARSER + : PatternGrammar.PATTERN_CHAR_PARSER; + List parsedRanges = (List) parser.parse(input).get(); + List ranges = ignoreCase + ? expandCase(parsedRanges, unicode) + : parsedRanges; + CharacterPredicate predicate = CharacterPredicate.optimizedRanges(ranges, unicode); + if (isNegated) { + predicate = predicate.not(); + } + if (message == null) { + message = "[" + toReadableString(pattern) + "]" + + (ignoreCase ? " (case-insensitive)" : "") + " expected"; + } + return of(predicate, message, unicode); + } + + private static List expandCase( + List ranges, boolean unicode) { + List result = new ArrayList<>(); + for (RangeCharPredicate range : ranges) { + result.add(range); + if (range.getStart() <= 0 && range.getStop() >= (unicode ? 0x10ffff : 0xffff)) { + continue; + } + for (int code = range.getStart(); code <= range.getStop(); code++) { + if (unicode) { + String str = new String(Character.toChars(code)); + String lower = str.toLowerCase(); + String upper = str.toUpperCase(); + if (!lower.equals(str) && lower.codePointCount(0, lower.length()) == 1) { + int cp = lower.codePointAt(0); + result.add(new RangeCharPredicate(cp, cp)); + } + if (!upper.equals(str) && upper.codePointCount(0, upper.length()) == 1) { + int cp = upper.codePointAt(0); + result.add(new RangeCharPredicate(cp, cp)); + } + } else { + char c = (char) code; + char lower = Character.toLowerCase(c); + char upper = Character.toUpperCase(c); + if (lower != c) { + result.add(new RangeCharPredicate(lower, lower)); + } + if (upper != c) { + result.add(new RangeCharPredicate(upper, upper)); + } + } + } + } + return result; + } + + private static class PatternGrammar { + static final Parser PATTERN_CHAR_PARSER = createPatternParser(false); + static final Parser PATTERN_UNICODE_PARSER = createPatternParser(true); + + private static Parser createPatternParser(boolean unicode) { + Parser character = unicode ? any(true) : any(false); + Parser single = character.map(val -> { + int cp = (val instanceof Character) + ? (Character) val + : ((String) val).codePointAt(0); + return new RangeCharPredicate(cp, cp); + }); + Parser range = character.seq(of('-')).seq(character).map((List values) -> { + int start = (values.get(0) instanceof Character) + ? (Character) values.get(0) + : ((String) values.get(0)).codePointAt(0); + int stop = (values.get(2) instanceof Character) + ? (Character) values.get(2) + : ((String) values.get(2)).codePointAt(0); + return new RangeCharPredicate(start, stop); + }); + return range.or(single).star().end(); + } } /** * Returns a parser that accepts a specific character range. */ public static CharacterParser range(char start, char stop) { - return range(start, stop, - toReadableString(start) + ".." + toReadableString(stop) + " expected"); + return range(start, stop, false); + } + + public static CharacterParser range(char start, char stop, boolean unicode) { + return range((int) start, (int) stop, unicode); } public static CharacterParser range(char start, char stop, String message) { - return of(CharacterPredicate.range(start, stop), message); + return range((int) start, (int) stop, message, false); + } + + public static CharacterParser range(char start, char stop, String message, boolean unicode) { + return range((int) start, (int) stop, message, unicode); + } + + public static CharacterParser range(int start, int stop, boolean unicode) { + return range(start, stop, + toReadableString(start) + ".." + toReadableString(stop) + " expected", unicode); + } + + public static CharacterParser range(int start, int stop, String message, boolean unicode) { + return of(new RangeCharPredicate(start, stop), message, unicode); } /** @@ -144,7 +311,7 @@ public static CharacterParser upperCase() { } public static CharacterParser upperCase(String message) { - return of(Character::isUpperCase, message); + return of(UppercaseCharPredicate.INSTANCE, message); } /** @@ -155,7 +322,7 @@ public static CharacterParser whitespace() { } public static CharacterParser whitespace(String message) { - return of(Character::isWhitespace, message); + return of(WhitespaceCharPredicate.INSTANCE, message); } /** @@ -166,13 +333,13 @@ public static CharacterParser word() { } public static CharacterParser word(String message) { - return of(Character::isLetterOrDigit, message); + return of(WordCharPredicate.INSTANCE, message); } private final CharacterPredicate matcher; private final String message; - private CharacterParser(CharacterPredicate matcher, String message) { + protected CharacterParser(CharacterPredicate matcher, String message) { this.matcher = Objects.requireNonNull(matcher, "Undefined matcher"); this.message = Objects.requireNonNull(message, "Undefined message"); } @@ -196,6 +363,55 @@ public int fastParseOn(String buffer, int position) { position + 1 : -1; } + public CharacterPredicate getMatcher() { + return matcher; + } + + public String getMessage() { + return message; + } + + @Override + public RepeatingCharacterParser starString() { + return repeatString(0, org.petitparser.parser.repeating.RepeatingParser.UNBOUNDED); + } + + @Override + public RepeatingCharacterParser starString(String message) { + return repeatString(0, org.petitparser.parser.repeating.RepeatingParser.UNBOUNDED, message); + } + + @Override + public RepeatingCharacterParser plusString() { + return repeatString(1, org.petitparser.parser.repeating.RepeatingParser.UNBOUNDED); + } + + @Override + public RepeatingCharacterParser plusString(String message) { + return repeatString(1, org.petitparser.parser.repeating.RepeatingParser.UNBOUNDED, message); + } + + @Override + public RepeatingCharacterParser timesString(int count) { + return repeatString(count, count); + } + + @Override + public RepeatingCharacterParser timesString(int count, String message) { + return repeatString(count, count, message); + } + + @Override + public RepeatingCharacterParser repeatString(int min, int max) { + return repeatString(min, max, null); + } + + @Override + public RepeatingCharacterParser repeatString(int min, int max, String message) { + return new RepeatingCharacterParser( + matcher, message != null ? message : this.message, min, max); + } + @Override public CharacterParser neg(String message) { // Return an optimized version of the receiver. @@ -204,9 +420,10 @@ public CharacterParser neg(String message) { @Override protected boolean hasEqualProperties(Parser other) { + CharacterParser that = (CharacterParser) other; return super.hasEqualProperties(other) && - Objects.equals(matcher, ((CharacterParser) other).matcher) && - Objects.equals(message, ((CharacterParser) other).message); + (matcher == that.matcher || matcher.isEqualTo(that.matcher)) && + Objects.equals(message, that.message); } @Override @@ -219,10 +436,19 @@ public String toString() { return super.toString() + "[" + message + "]"; } - private static String toReadableString(String characters) { + public static String toReadableString(int codePoint) { + if (codePoint <= Character.MAX_VALUE) { + return toReadableString((char) codePoint); + } + return new String(Character.toChars(codePoint)); + } + + public static String toReadableString(String characters) { StringBuilder buffer = new StringBuilder(); - for (int i = 0; i < characters.length(); i++) { - buffer.append(toReadableString(characters.charAt(i))); + for (int i = 0; i < characters.length(); ) { + int cp = characters.codePointAt(i); + buffer.append(toReadableString(cp)); + i += Character.charCount(cp); } return buffer.toString(); } diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterPredicate.java index 880be8b..61fd29e 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterPredicate.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterPredicate.java @@ -2,9 +2,9 @@ import org.petitparser.parser.Parser; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; +import java.util.Locale; /** * Character predicate. @@ -16,7 +16,7 @@ public interface CharacterPredicate { * Returns a character predicate that matches any character. */ static CharacterPredicate any() { - return value -> true; + return ConstantCharPredicate.any(); } /** @@ -24,17 +24,30 @@ static CharacterPredicate any() { * string}. */ static CharacterPredicate anyOf(String string) { - List ranges = string.chars() - .mapToObj(value -> new CharacterRange((char) value, (char) value)) - .collect(Collectors.toList()); - return CharacterRange.toCharacterPredicate(ranges); + return optimizedString(string); + } + + /** + * Returns a character predicate that matches any of the characters in {@code + * string} with unicode option. + */ + static CharacterPredicate anyOf(String string, boolean unicode) { + return optimizedString(string, false, unicode); + } + + /** + * Returns a character predicate that matches any of the characters in {@code + * string} with case-insensitivity and unicode options. + */ + static CharacterPredicate anyOf(String string, boolean ignoreCase, boolean unicode) { + return optimizedString(string, ignoreCase, unicode); } /** * Returns a character predicate that matches no character. */ static CharacterPredicate none() { - return value -> false; + return ConstantCharPredicate.none(); } /** @@ -42,17 +55,37 @@ static CharacterPredicate none() { * string}. */ static CharacterPredicate noneOf(String string) { - List ranges = string.chars() - .mapToObj(value -> new CharacterRange((char) value, (char) value)) - .collect(Collectors.toList()); - return CharacterRange.toCharacterPredicate(ranges).not(); + return anyOf(string).not(); + } + + /** + * Returns a character predicate that matches none of the characters in {@code + * string} with unicode option. + */ + static CharacterPredicate noneOf(String string, boolean unicode) { + return anyOf(string, unicode).not(); + } + + /** + * Returns a character predicate that matches none of the characters in {@code + * string} with case-insensitivity and unicode options. + */ + static CharacterPredicate noneOf(String string, boolean ignoreCase, boolean unicode) { + return anyOf(string, ignoreCase, unicode).not(); } /** * Returns a character predicate that matches the given {@code character}. */ static CharacterPredicate of(char character) { - return value -> value == character; + return new SingleCharPredicate(character); + } + + /** + * Returns a character predicate that matches the given code point. + */ + static CharacterPredicate of(int codePoint) { + return new SingleCharPredicate(codePoint); } /** @@ -60,7 +93,15 @@ static CharacterPredicate of(char character) { * start} and {@code stop}. */ static CharacterPredicate range(char start, char stop) { - return value -> start <= value && value <= stop; + return new RangeCharPredicate(start, stop); + } + + /** + * Returns a character predicate that matches any code point between {@code + * start} and {@code stop}. + */ + static CharacterPredicate range(int start, int stop) { + return new RangeCharPredicate(start, stop); } /** @@ -68,22 +109,15 @@ static CharacterPredicate range(char start, char stop) { * starts} and {@code stops}. */ static CharacterPredicate ranges(char[] starts, char[] stops) { - if (starts.length != stops.length) { - throw new IllegalArgumentException("Invalid range sizes."); - } - for (int i = 0; i < starts.length; i++) { - if (starts[i] > stops[i]) { - throw new IllegalArgumentException( - "Invalid range: " + starts[i] + "-" + stops[i]); - } - if (i + 1 < starts.length && starts[i + 1] <= stops[i]) { - throw new IllegalArgumentException("Invalid sequence."); - } - } - return value -> { - int index = Arrays.binarySearch(starts, value); - return index >= 0 || index < -1 && value <= stops[-index - 2]; - }; + return new RangesCharPredicate(starts, stops); + } + + /** + * Returns a character predicate that matches code point ranges between {@code + * starts} and {@code stops}. + */ + static CharacterPredicate ranges(int[] starts, int[] stops) { + return new RangesCharPredicate(starts, stops); } /** @@ -93,6 +127,117 @@ static CharacterPredicate pattern(String pattern) { return PatternParser.PATTERN.parse(pattern).get(); } + /** + * Returns a character predicate that matches the provided pattern with unicode option. + */ + static CharacterPredicate pattern(String pattern, boolean unicode) { + return CharacterParser.pattern(pattern, unicode).getMatcher(); + } + + /** + * Returns a character predicate that matches the provided pattern with case-insensitivity and unicode options. + */ + static CharacterPredicate pattern(String pattern, boolean ignoreCase, boolean unicode) { + return CharacterParser.pattern(pattern, null, ignoreCase, unicode).getMatcher(); + } + + /** + * Creates an optimized character predicate from a string. + */ + static CharacterPredicate optimizedString(String string) { + return optimizedString(string, false, false); + } + + /** + * Creates an optimized character predicate from a string with case-insensitivity option. + */ + static CharacterPredicate optimizedString(String string, boolean ignoreCase) { + return optimizedString(string, ignoreCase, false); + } + + /** + * Creates an optimized character predicate from a string with case-insensitivity and unicode options. + */ + static CharacterPredicate optimizedString( + String string, boolean ignoreCase, boolean unicode) { + if (ignoreCase) { + string = string.toLowerCase(Locale.ROOT) + string.toUpperCase(Locale.ROOT); + } + List ranges = new ArrayList<>(); + if (unicode) { + string.codePoints().forEach(cp -> ranges.add(new RangeCharPredicate(cp, cp))); + } else { + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + ranges.add(new RangeCharPredicate(c, c)); + } + } + return optimizedRanges(ranges, unicode); + } + + /** + * Creates an optimized character predicate from a list of range predicates. + */ + static CharacterPredicate optimizedRanges(List ranges) { + return optimizedRanges(ranges, false); + } + + /** + * Creates an optimized character predicate from a list of range predicates with unicode option. + */ + static CharacterPredicate optimizedRanges( + List ranges, boolean unicode) { + // 1. Sort the ranges + List sortedRanges = new ArrayList<>(ranges); + sortedRanges.sort((first, second) -> { + if (first.getStart() != second.getStart()) { + return Integer.compare(first.getStart(), second.getStart()); + } + return Integer.compare(first.getStop(), second.getStop()); + }); + + // 2. Merge adjacent or overlapping ranges + List mergedRanges = new ArrayList<>(); + for (RangeCharPredicate thisRange : sortedRanges) { + if (mergedRanges.isEmpty()) { + mergedRanges.add(thisRange); + } else { + RangeCharPredicate lastRange = mergedRanges.getLast(); + if ((long) lastRange.getStop() + 1 >= thisRange.getStart()) { + RangeCharPredicate merged = new RangeCharPredicate( + lastRange.getStart(), + Math.max(lastRange.getStop(), thisRange.getStop())); + mergedRanges.set(mergedRanges.size() - 1, merged); + } else { + mergedRanges.add(thisRange); + } + } + } + + // 3. Build the best resulting predicate + if (mergedRanges.isEmpty()) { + return ConstantCharPredicate.none(); + } else if (mergedRanges.size() == 1) { + RangeCharPredicate range = mergedRanges.getFirst(); + if (range.getStart() <= 0 && + range.getStop() >= (unicode ? 0x10ffff : 0xffff)) { + return ConstantCharPredicate.any(); + } else if (range.getStart() == range.getStop()) { + return new SingleCharPredicate(range.getStart()); + } else { + return range; + } + } else { + int lookupBytes = (mergedRanges.getLast().getStop() + - mergedRanges.getFirst().getStart() + 32) >> 3; + int rangesBytes = mergedRanges.size() * 8; + if (lookupBytes > 1024 && rangesBytes < (lookupBytes >> 3)) { + return RangesCharPredicate.fromRanges(mergedRanges); + } + return LookupCharPredicate.fromRanges(mergedRanges); + } + } + class PatternParser { static final Parser PATTERN_SIMPLE = CharacterParser.any() .map((Character value) -> new CharacterRange(value, value)); @@ -117,32 +262,24 @@ class PatternParser { */ boolean test(char value); + /** + * Tests if the character predicate is satisfied for a Unicode code point. + */ + default boolean test(int value) { + return value >= 0 && value <= Character.MAX_VALUE && test((char) value); + } + /** * Negates this character predicate. */ default CharacterPredicate not() { - return new NotCharacterPredicate(this); + return new NotCharPredicate(this); } /** - * The negated character predicate. + * Tests for structural equality of two character predicates. */ - class NotCharacterPredicate implements CharacterPredicate { - - private final CharacterPredicate predicate; - - public NotCharacterPredicate(CharacterPredicate predicate) { - this.predicate = predicate; - } - - @Override - public boolean test(char value) { - return !predicate.test(value); - } - - @Override - public CharacterPredicate not() { - return predicate; - } + default boolean isEqualTo(CharacterPredicate other) { + return equals(other); } } diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterRange.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterRange.java index 07cf5b2..f58d606 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterRange.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/CharacterRange.java @@ -1,7 +1,6 @@ package org.petitparser.parser.primitive; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; /** @@ -10,54 +9,16 @@ */ class CharacterRange { - static final Comparator CHARACTER_RANGE_COMPARATOR = - Comparator.comparing((CharacterRange range) -> range.start) - .thenComparing((CharacterRange range) -> range.stop); - static CharacterPredicate toCharacterPredicate(List ranges) { - - // 1. sort the ranges - List sortedRanges = new ArrayList<>(ranges); - sortedRanges.sort(CHARACTER_RANGE_COMPARATOR); - - // 2. merge adjacent or overlapping ranges - List mergedRanges = new ArrayList<>(); - for (CharacterRange thisRange : sortedRanges) { - if (mergedRanges.isEmpty()) { - mergedRanges.add(thisRange); - } else { - CharacterRange lastRange = mergedRanges.get(mergedRanges.size() - 1); - if (lastRange.stop + 1 >= thisRange.start) { - CharacterRange characterRange = - new CharacterRange(lastRange.start, thisRange.stop); - mergedRanges.set(mergedRanges.size() - 1, characterRange); - } else { - mergedRanges.add(thisRange); - } - } - } - - // 3. build the best resulting predicates - if (mergedRanges.isEmpty()) { - return CharacterPredicate.none(); - } else if (mergedRanges.size() == 1) { - CharacterRange characterRange = mergedRanges.get(0); - return characterRange.start == characterRange.stop ? - CharacterPredicate.of(characterRange.start) : - CharacterPredicate.range(characterRange.start, characterRange.stop); - } else { - char[] starts = new char[mergedRanges.size()]; - char[] stops = new char[mergedRanges.size()]; - for (int i = 0; i < mergedRanges.size(); i++) { - starts[i] = mergedRanges.get(i).start; - stops[i] = mergedRanges.get(i).stop; - } - return CharacterPredicate.ranges(starts, stops); + List list = new ArrayList<>(ranges.size()); + for (CharacterRange range : ranges) { + list.add(new RangeCharPredicate(range.start, range.stop)); } + return CharacterPredicate.optimizedRanges(list, false); } - private final char start; - private final char stop; + final char start; + final char stop; CharacterRange(char start, char stop) { this.start = start; diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/ConstantCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/ConstantCharPredicate.java new file mode 100644 index 0000000..afc81e6 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/ConstantCharPredicate.java @@ -0,0 +1,65 @@ +package org.petitparser.parser.primitive; + +/** + * Character predicate returning a constant boolean value. + */ +public class ConstantCharPredicate implements CharacterPredicate { + + public static final ConstantCharPredicate ANY = new ConstantCharPredicate(true); + public static final ConstantCharPredicate NONE = new ConstantCharPredicate(false); + + public static ConstantCharPredicate any() { + return ANY; + } + + public static ConstantCharPredicate none() { + return NONE; + } + + private final boolean result; + + public ConstantCharPredicate(boolean result) { + this.result = result; + } + + public boolean getResult() { + return result; + } + + @Override + public boolean test(char value) { + return result; + } + + @Override + public boolean test(int value) { + return result; + } + + @Override + public CharacterPredicate not() { + return result ? NONE : ANY; + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof ConstantCharPredicate && + result == ((ConstantCharPredicate) other).result; + } + + @Override + public boolean equals(Object other) { + return other instanceof CharacterPredicate && + isEqualTo((CharacterPredicate) other); + } + + @Override + public int hashCode() { + return Boolean.hashCode(result); + } + + @Override + public String toString() { + return "ConstantCharPredicate[" + result + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/DigitCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/DigitCharPredicate.java new file mode 100644 index 0000000..1e0da2b --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/DigitCharPredicate.java @@ -0,0 +1,39 @@ +package org.petitparser.parser.primitive; + +/** + * Character predicate matching digit characters. + */ +public class DigitCharPredicate implements CharacterPredicate { + + public static final DigitCharPredicate INSTANCE = new DigitCharPredicate(); + + @Override + public boolean test(char value) { + return Character.isDigit(value); + } + + @Override + public boolean test(int value) { + return Character.isDigit(value); + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof DigitCharPredicate; + } + + @Override + public boolean equals(Object other) { + return other instanceof DigitCharPredicate; + } + + @Override + public int hashCode() { + return DigitCharPredicate.class.hashCode(); + } + + @Override + public String toString() { + return "DigitCharPredicate"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/EpsilonParser.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/EpsilonParser.java index 4c819fb..c3bb00c 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/primitive/EpsilonParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/EpsilonParser.java @@ -4,14 +4,46 @@ import org.petitparser.context.Result; import org.petitparser.parser.Parser; +import java.util.Objects; + /** * A parser that consumes nothing and always succeeds. */ public class EpsilonParser extends Parser { + /** + * Reusable singleton instance of {@link EpsilonParser} returning {@code null}. + */ + public static final EpsilonParser INSTANCE = new EpsilonParser(); + + protected final Object value; + + /** + * Constructs an epsilon parser that produces {@code null}. + */ + public EpsilonParser() { + this(null); + } + + /** + * Constructs an epsilon parser that produces {@code value}. + * + * @param value the result value. + */ + public EpsilonParser(Object value) { + this.value = value; + } + + /** + * Returns the value produced on success. + */ + public Object getValue() { + return value; + } + @Override public Result parseOn(Context context) { - return context.success(null); + return context.success(value); } @Override @@ -21,6 +53,17 @@ public int fastParseOn(String buffer, int position) { @Override public EpsilonParser copy() { - return new EpsilonParser(); + return new EpsilonParser(value); + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && + Objects.equals(value, ((EpsilonParser) other).value); + } + + @Override + public String toString() { + return value == null ? super.toString() : super.toString() + "[" + value + "]"; } } diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/FailureParser.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/FailureParser.java index 7382bf8..82f1ad9 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/primitive/FailureParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/FailureParser.java @@ -12,8 +12,7 @@ public class FailureParser extends Parser { /** - * Construct a {@link FailureParser} that fails with the supplied {@code - * message}. + * Construct a {@link FailureParser} that fails with the supplied {@code message}. */ public static Parser withMessage(String message) { return new FailureParser(message); @@ -21,10 +20,29 @@ public static Parser withMessage(String message) { private final String message; - private FailureParser(String message) { + /** + * Constructs a failure parser with a default error message. + */ + public FailureParser() { + this("unable to parse"); + } + + /** + * Constructs a failure parser with the specified {@code message}. + * + * @param message the failure message. + */ + public FailureParser(String message) { this.message = Objects.requireNonNull(message, "Undefined message"); } + /** + * Returns the failure message. + */ + public String getMessage() { + return message; + } + @Override public Result parseOn(Context context) { return context.failure(message); diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/LetterCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/LetterCharPredicate.java new file mode 100644 index 0000000..092afa8 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/LetterCharPredicate.java @@ -0,0 +1,39 @@ +package org.petitparser.parser.primitive; + +/** + * Character predicate matching letter characters. + */ +public class LetterCharPredicate implements CharacterPredicate { + + public static final LetterCharPredicate INSTANCE = new LetterCharPredicate(); + + @Override + public boolean test(char value) { + return Character.isLetter(value); + } + + @Override + public boolean test(int value) { + return Character.isLetter(value); + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof LetterCharPredicate; + } + + @Override + public boolean equals(Object other) { + return other instanceof LetterCharPredicate; + } + + @Override + public int hashCode() { + return LetterCharPredicate.class.hashCode(); + } + + @Override + public String toString() { + return "LetterCharPredicate"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/LookupCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/LookupCharPredicate.java new file mode 100644 index 0000000..6e26eb4 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/LookupCharPredicate.java @@ -0,0 +1,101 @@ +package org.petitparser.parser.primitive; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Character predicate performing O(1) bitset lookup. + */ +public class LookupCharPredicate implements CharacterPredicate { + + private final int start; + private final int stop; + private final int[] bits; + + public LookupCharPredicate(int start, int stop, int[] bits) { + if (start > stop) { + throw new IllegalArgumentException("Invalid range: " + start + "-" + stop); + } + this.start = start; + this.stop = stop; + this.bits = bits.clone(); + } + + public LookupCharPredicate(boolean[] table) { + if (table.length == 0) { + throw new IllegalArgumentException("Table cannot be empty"); + } + this.start = 0; + this.stop = table.length - 1; + this.bits = new int[(table.length + 31) >> 5]; + for (int i = 0; i < table.length; i++) { + if (table[i]) { + this.bits[i >> 5] |= (1 << (i & 31)); + } + } + } + + public static LookupCharPredicate fromRanges(List ranges) { + if (ranges.isEmpty()) { + throw new IllegalArgumentException("Empty ranges"); + } + int start = ranges.get(0).getStart(); + int stop = ranges.get(ranges.size() - 1).getStop(); + int size = ((stop - start) + 32) >> 5; + int[] bits = new int[size]; + for (RangeCharPredicate range : ranges) { + for (int i = range.getStart() - start; i <= range.getStop() - start; i++) { + bits[i >> 5] |= (1 << (i & 31)); + } + } + return new LookupCharPredicate(start, stop, bits); + } + + public int getStart() { + return start; + } + + public int getStop() { + return stop; + } + + public int[] getBits() { + return bits.clone(); + } + + @Override + public boolean test(char value) { + return test((int) value); + } + + @Override + public boolean test(int value) { + return start <= value && value <= stop && + (bits[(value - start) >> 5] & (1 << ((value - start) & 31))) != 0; + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof LookupCharPredicate that && + start == that.start && + stop == that.stop && + Arrays.equals(bits, that.bits); + } + + @Override + public boolean equals(Object other) { + return other instanceof CharacterPredicate that && + isEqualTo(that); + } + + @Override + public int hashCode() { + return Objects.hash(start, stop, Arrays.hashCode(bits)); + } + + @Override + public String toString() { + return "LookupCharPredicate[" + start + ".." + stop + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/LowercaseCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/LowercaseCharPredicate.java new file mode 100644 index 0000000..c4d259b --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/LowercaseCharPredicate.java @@ -0,0 +1,39 @@ +package org.petitparser.parser.primitive; + +/** + * Character predicate matching lowercase characters. + */ +public class LowercaseCharPredicate implements CharacterPredicate { + + public static final LowercaseCharPredicate INSTANCE = new LowercaseCharPredicate(); + + @Override + public boolean test(char value) { + return Character.isLowerCase(value); + } + + @Override + public boolean test(int value) { + return Character.isLowerCase(value); + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof LowercaseCharPredicate; + } + + @Override + public boolean equals(Object other) { + return other instanceof LowercaseCharPredicate; + } + + @Override + public int hashCode() { + return LowercaseCharPredicate.class.hashCode(); + } + + @Override + public String toString() { + return "LowercaseCharPredicate"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/NewlineParser.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/NewlineParser.java new file mode 100644 index 0000000..8946466 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/NewlineParser.java @@ -0,0 +1,93 @@ +package org.petitparser.parser.primitive; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; + +import java.util.Objects; + +/** + * A parser that consumes newlines platform independently. + */ +public class NewlineParser extends Parser { + + /** + * Reusable singleton instance of {@link NewlineParser} with default message. + */ + public static final NewlineParser INSTANCE = new NewlineParser(); + + protected final String message; + + /** + * Constructs a newline parser with a default failure message. + */ + public NewlineParser() { + this("newline expected"); + } + + /** + * Constructs a newline parser with a custom failure message. + * + * @param message the failure message. + */ + public NewlineParser(String message) { + this.message = Objects.requireNonNull(message, "Undefined message"); + } + + /** + * Returns the failure message. + */ + public String getMessage() { + return message; + } + + @Override + public Result parseOn(Context context) { + String buffer = context.getBuffer(); + int position = context.getPosition(); + if (position < buffer.length()) { + switch (buffer.charAt(position)) { + case '\n': + return context.success("\n", position + 1); + case '\r': + if (position + 1 < buffer.length() && buffer.charAt(position + 1) == '\n') { + return context.success("\r\n", position + 2); + } else { + return context.success("\r", position + 1); + } + } + } + return context.failure(message); + } + + @Override + public int fastParseOn(String buffer, int position) { + if (position < buffer.length()) { + switch (buffer.charAt(position)) { + case '\n': + return position + 1; + case '\r': + return position + 1 < buffer.length() && buffer.charAt(position + 1) == '\n' + ? position + 2 + : position + 1; + } + } + return -1; + } + + @Override + public String toString() { + return super.toString() + "[" + message + "]"; + } + + @Override + public NewlineParser copy() { + return new NewlineParser(message); + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && + Objects.equals(message, ((NewlineParser) other).message); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/NotCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/NotCharPredicate.java new file mode 100644 index 0000000..1d193ba --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/NotCharPredicate.java @@ -0,0 +1,56 @@ +package org.petitparser.parser.primitive; + +import java.util.Objects; + +/** + * Character predicate negating another predicate. + */ +public class NotCharPredicate implements CharacterPredicate { + + private final CharacterPredicate predicate; + + public NotCharPredicate(CharacterPredicate predicate) { + this.predicate = Objects.requireNonNull(predicate, "Undefined predicate"); + } + + public CharacterPredicate getPredicate() { + return predicate; + } + + @Override + public boolean test(char value) { + return !predicate.test(value); + } + + @Override + public boolean test(int value) { + return !predicate.test(value); + } + + @Override + public CharacterPredicate not() { + return predicate; + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof NotCharPredicate that && + predicate.isEqualTo(that.predicate); + } + + @Override + public boolean equals(Object other) { + return other instanceof CharacterPredicate that && + isEqualTo(that); + } + + @Override + public int hashCode() { + return Objects.hash(predicate); + } + + @Override + public String toString() { + return "NotCharPredicate[" + predicate + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/PositionParser.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/PositionParser.java new file mode 100644 index 0000000..0c451d1 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/PositionParser.java @@ -0,0 +1,31 @@ +package org.petitparser.parser.primitive; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; + +/** + * A parser that reports the current input position. + */ +public class PositionParser extends Parser { + + /** + * Reusable singleton instance of {@link PositionParser}. + */ + public static final PositionParser INSTANCE = new PositionParser(); + + @Override + public Result parseOn(Context context) { + return context.success(context.getPosition()); + } + + @Override + public int fastParseOn(String buffer, int position) { + return position; + } + + @Override + public PositionParser copy() { + return new PositionParser(); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/RangeCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/RangeCharPredicate.java new file mode 100644 index 0000000..6ce687a --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/RangeCharPredicate.java @@ -0,0 +1,73 @@ +package org.petitparser.parser.primitive; + +import java.util.Objects; + +/** + * Character predicate matching a range of characters. + */ +public class RangeCharPredicate implements CharacterPredicate { + + public static CharacterPredicate optimizedRanges(java.util.List ranges) { + return CharacterPredicate.optimizedRanges(ranges); + } + + public static CharacterPredicate optimizedRanges( + java.util.List ranges, boolean unicode) { + return CharacterPredicate.optimizedRanges(ranges, unicode); + } + + private final int start; + private final int stop; + + public RangeCharPredicate(char start, char stop) { + this((int) start, (int) stop); + } + + public RangeCharPredicate(int start, int stop) { + if (start > stop) { + throw new IllegalArgumentException("Invalid range: " + start + "-" + stop); + } + this.start = start; + this.stop = stop; + } + + public int getStart() { + return start; + } + + public int getStop() { + return stop; + } + + @Override + public boolean test(char value) { + return start <= value && value <= stop; + } + + @Override + public boolean test(int value) { + return start <= value && value <= stop; + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof RangeCharPredicate that && + start == that.start && + stop == that.stop; + } + + @Override + public boolean equals(Object other) { + return other instanceof CharacterPredicate that && isEqualTo(that); + } + + @Override + public int hashCode() { + return Objects.hash(start, stop); + } + + @Override + public String toString() { + return "RangeCharPredicate[" + start + ".." + stop + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/RangesCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/RangesCharPredicate.java new file mode 100644 index 0000000..b9c9131 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/RangesCharPredicate.java @@ -0,0 +1,102 @@ +package org.petitparser.parser.primitive; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Character predicate matching multiple non-overlapping sorted ranges of characters. + */ +public class RangesCharPredicate implements CharacterPredicate { + + private final int[] starts; + private final int[] stops; + + public RangesCharPredicate(char[] starts, char[] stops) { + if (starts.length != stops.length) { + throw new IllegalArgumentException("Invalid range sizes."); + } + this.starts = new int[starts.length]; + this.stops = new int[stops.length]; + for (int i = 0; i < starts.length; i++) { + if (starts[i] > stops[i]) { + throw new IllegalArgumentException( + "Invalid range: " + starts[i] + "-" + stops[i]); + } + if (i + 1 < starts.length && starts[i + 1] <= stops[i]) { + throw new IllegalArgumentException("Invalid sequence."); + } + this.starts[i] = starts[i]; + this.stops[i] = stops[i]; + } + } + + public RangesCharPredicate(int[] starts, int[] stops) { + if (starts.length != stops.length) { + throw new IllegalArgumentException("Invalid range sizes."); + } + for (int i = 0; i < starts.length; i++) { + if (starts[i] > stops[i]) { + throw new IllegalArgumentException( + "Invalid range: " + starts[i] + "-" + stops[i]); + } + if (i + 1 < starts.length && starts[i + 1] <= stops[i]) { + throw new IllegalArgumentException("Invalid sequence."); + } + } + this.starts = starts.clone(); + this.stops = stops.clone(); + } + + public static RangesCharPredicate fromRanges(List ranges) { + int[] starts = new int[ranges.size()]; + int[] stops = new int[ranges.size()]; + for (int i = 0; i < ranges.size(); i++) { + starts[i] = ranges.get(i).getStart(); + stops[i] = ranges.get(i).getStop(); + } + return new RangesCharPredicate(starts, stops); + } + + public int[] getStarts() { + return starts.clone(); + } + + public int[] getStops() { + return stops.clone(); + } + + @Override + public boolean test(char value) { + return test((int) value); + } + + @Override + public boolean test(int value) { + int index = Arrays.binarySearch(starts, value); + return index >= 0 || index < -1 && value <= stops[-index - 2]; + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof RangesCharPredicate that && + Arrays.equals(starts, that.starts) && + Arrays.equals(stops, that.stops); + } + + @Override + public boolean equals(Object other) { + return other instanceof CharacterPredicate that && + isEqualTo(that); + } + + @Override + public int hashCode() { + return Objects.hash(Arrays.hashCode(starts), Arrays.hashCode(stops)); + } + + @Override + public String toString() { + return "RangesCharPredicate[" + starts.length + " ranges]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/SingleCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/SingleCharPredicate.java new file mode 100644 index 0000000..f8c7cb4 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/SingleCharPredicate.java @@ -0,0 +1,51 @@ +package org.petitparser.parser.primitive; + +/** + * Character predicate matching a single character. + */ +public class SingleCharPredicate implements CharacterPredicate { + + private final int value; + + public SingleCharPredicate(char value) { + this((int) value); + } + + public SingleCharPredicate(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + @Override + public boolean test(char value) { + return value == this.value; + } + + @Override + public boolean test(int value) { + return value == this.value; + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof SingleCharPredicate that && value == that.value; + } + + @Override + public boolean equals(Object other) { + return other instanceof CharacterPredicate that && isEqualTo(that); + } + + @Override + public int hashCode() { + return Integer.hashCode(value); + } + + @Override + public String toString() { + return "SingleCharPredicate[" + value + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/StringIgnoreCaseParser.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/StringIgnoreCaseParser.java new file mode 100644 index 0000000..386d9c9 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/StringIgnoreCaseParser.java @@ -0,0 +1,83 @@ +package org.petitparser.parser.primitive; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; + +import java.util.Objects; + +/** + * Parses a sequence of characters ignoring case. + */ +public class StringIgnoreCaseParser extends Parser { + + /** + * Construct a parser that accepts the provided {@link String} {@code value} + * case-insensitively. + */ + public static StringIgnoreCaseParser of(String value) { + return of(value, value + " expected"); + } + + /** + * Construct a parser that accepts the provided {@link String} {@code value} + * case-insensitively, failing with {@code message}. + */ + public static StringIgnoreCaseParser of(String value, String message) { + return new StringIgnoreCaseParser(value, message); + } + + private final String value; + private final String message; + + public StringIgnoreCaseParser(String value, String message) { + this.value = Objects.requireNonNull(value, "Undefined value"); + this.message = Objects.requireNonNull(message, "Undefined message"); + } + + public String getValue() { + return value; + } + + public String getMessage() { + return message; + } + + @Override + public Result parseOn(Context context) { + String buffer = context.getBuffer(); + int start = context.getPosition(); + int size = value.length(); + int stop = start + size; + if (stop <= buffer.length() && + buffer.regionMatches(true, start, value, 0, size)) { + return context.success(buffer.substring(start, stop), stop); + } + return context.failure(message); + } + + @Override + public int fastParseOn(String buffer, int position) { + int size = value.length(); + return position + size <= buffer.length() && + buffer.regionMatches(true, position, value, 0, size) ? position + size : -1; + } + + @Override + protected boolean hasEqualProperties(Parser other) { + StringIgnoreCaseParser that = (StringIgnoreCaseParser) other; + return super.hasEqualProperties(other) && + value.equalsIgnoreCase(that.value) && + Objects.equals(message, that.message); + } + + @Override + public StringIgnoreCaseParser copy() { + return new StringIgnoreCaseParser(value, message); + } + + @Override + public String toString() { + return super.toString() + "[" + message + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/StringParser.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/StringParser.java index fa3c815..76982c4 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/primitive/StringParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/StringParser.java @@ -5,7 +5,6 @@ import org.petitparser.parser.Parser; import java.util.Objects; -import java.util.function.Predicate; /** * Parses a sequence of characters. @@ -13,79 +12,78 @@ public class StringParser extends Parser { /** - * Construct a parser that accepts the provides {@link String} {@code value}. + * Construct a parser that accepts the provided {@link String} {@code value}. */ - public static Parser of(String value) { + public static StringParser of(String value) { return of(value, value + " expected"); } /** - * Construct a parser that accepts the provides {@link String} {@code value }, - * and that fails with the error provided error {@code message}. + * Construct a parser that accepts the provided {@link String} {@code value}, + * and that fails with the provided error {@code message}. */ - public static Parser of(String value, String message) { - return new StringParser(value.length(), value::equals, message); + public static StringParser of(String value, String message) { + return new StringParser(value, message); } /** - * Construct a parser that accepts the provides {@link String} {@code value } + * Construct a parser that accepts the provided {@link String} {@code value} * case insensitive. */ - public static Parser ofIgnoringCase(String value) { - return ofIgnoringCase(value, value + " expected"); + public static StringIgnoreCaseParser ofIgnoringCase(String value) { + return StringIgnoreCaseParser.of(value); } /** - * Construct a parser that accepts the provides {@link String} {@code value } - * case insensitive, and that fails with the error provided error {@code - * message}. + * Construct a parser that accepts the provided {@link String} {@code value} + * case insensitive, and that fails with the provided error {@code message}. */ - public static Parser ofIgnoringCase(String value, String message) { - return new StringParser(value.length(), value::equalsIgnoreCase, message); + public static StringIgnoreCaseParser ofIgnoringCase(String value, String message) { + return StringIgnoreCaseParser.of(value, message); } - private final int size; - private final Predicate predicate; + private final String value; private final String message; - private StringParser(int size, Predicate predicate, String message) { - this.size = size; - this.predicate = Objects.requireNonNull(predicate, "Undefined predicate"); + public StringParser(String value, String message) { + this.value = Objects.requireNonNull(value, "Undefined value"); this.message = Objects.requireNonNull(message, "Undefined message"); } + public String getValue() { + return value; + } + + public String getMessage() { + return message; + } + @Override public Result parseOn(Context context) { String buffer = context.getBuffer(); int start = context.getPosition(); - int stop = start + size; - if (stop <= buffer.length()) { - String result = buffer.substring(start, stop); - if (predicate.test(result)) { - return context.success(result, stop); - } + if (buffer.startsWith(value, start)) { + return context.success(value, start + value.length()); } return context.failure(message); } @Override public int fastParseOn(String buffer, int position) { - int stop = position + size; - return stop <= buffer.length() && - predicate.test(buffer.substring(position, stop)) ? stop : -1; + return buffer.startsWith(value, position) ? position + value.length() : -1; } @Override protected boolean hasEqualProperties(Parser other) { + StringParser that = (StringParser) other; return super.hasEqualProperties(other) && - Objects.equals(size, ((StringParser) other).size) && - Objects.equals(predicate, ((StringParser) other).predicate) && - Objects.equals(message, ((StringParser) other).message); + Objects.equals(value, that.value) && + Objects.equals(message, that.message); } @Override public StringParser copy() { - return new StringParser(size, predicate, message); + return new StringParser(value, message); } @Override diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/UnicodeCharacterParser.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/UnicodeCharacterParser.java new file mode 100644 index 0000000..d3fcbf4 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/UnicodeCharacterParser.java @@ -0,0 +1,86 @@ +package org.petitparser.parser.primitive; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.repeating.RepeatingCharacterParser; + +/** + * Parser for an individual Unicode code point (including surrogate pairs) + * satisfying a specified {@link CharacterPredicate}. + */ +public class UnicodeCharacterParser extends CharacterParser { + + public UnicodeCharacterParser(CharacterPredicate matcher, String message) { + super(matcher, message); + } + + @Override + public Result parseOn(Context context) { + String buffer = context.getBuffer(); + int position = context.getPosition(); + if (position < buffer.length()) { + char ch = buffer.charAt(position); + int nextPosition = position + 1; + int codePoint = ch; + if (Character.isHighSurrogate(ch) && nextPosition < buffer.length()) { + char nextCh = buffer.charAt(nextPosition); + if (Character.isLowSurrogate(nextCh)) { + codePoint = Character.toCodePoint(ch, nextCh); + nextPosition++; + } + } + if (getMatcher().test(codePoint)) { + return context.success(buffer.substring(position, nextPosition), nextPosition); + } + } + return context.failure(getMessage()); + } + + @Override + public int fastParseOn(String buffer, int position) { + if (position < buffer.length()) { + char ch = buffer.charAt(position); + int nextPosition = position + 1; + int codePoint = ch; + if (Character.isHighSurrogate(ch) && nextPosition < buffer.length()) { + char nextCh = buffer.charAt(nextPosition); + if (Character.isLowSurrogate(nextCh)) { + codePoint = Character.toCodePoint(ch, nextCh); + nextPosition++; + } + } + if (getMatcher().test(codePoint)) { + return nextPosition; + } + } + return -1; + } + + @Override + public RepeatingCharacterParser repeatString(int min, int max, String message) { + return new RepeatingCharacterParser( + getMatcher(), message != null ? message : getMessage(), min, max, true); + } + + @Override + public UnicodeCharacterParser copy() { + return new UnicodeCharacterParser(getMatcher(), getMessage()); + } + + @Override + public UnicodeCharacterParser neg() { + return neg(this + " not expected"); + } + + @Override + public UnicodeCharacterParser neg(String message) { + return new UnicodeCharacterParser( + getMatcher().not(), message != null ? message : this + " not expected"); + } + + @Override + protected boolean hasEqualProperties(Parser other) { + return super.hasEqualProperties(other) && other instanceof UnicodeCharacterParser; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/UppercaseCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/UppercaseCharPredicate.java new file mode 100644 index 0000000..d717a0a --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/UppercaseCharPredicate.java @@ -0,0 +1,39 @@ +package org.petitparser.parser.primitive; + +/** + * Character predicate matching uppercase characters. + */ +public class UppercaseCharPredicate implements CharacterPredicate { + + public static final UppercaseCharPredicate INSTANCE = new UppercaseCharPredicate(); + + @Override + public boolean test(char value) { + return Character.isUpperCase(value); + } + + @Override + public boolean test(int value) { + return Character.isUpperCase(value); + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof UppercaseCharPredicate; + } + + @Override + public boolean equals(Object other) { + return other instanceof UppercaseCharPredicate; + } + + @Override + public int hashCode() { + return UppercaseCharPredicate.class.hashCode(); + } + + @Override + public String toString() { + return "UppercaseCharPredicate"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/WhitespaceCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/WhitespaceCharPredicate.java new file mode 100644 index 0000000..45068bd --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/WhitespaceCharPredicate.java @@ -0,0 +1,39 @@ +package org.petitparser.parser.primitive; + +/** + * Character predicate matching whitespace characters. + */ +public class WhitespaceCharPredicate implements CharacterPredicate { + + public static final WhitespaceCharPredicate INSTANCE = new WhitespaceCharPredicate(); + + @Override + public boolean test(char value) { + return Character.isWhitespace(value); + } + + @Override + public boolean test(int value) { + return Character.isWhitespace(value); + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof WhitespaceCharPredicate; + } + + @Override + public boolean equals(Object other) { + return other instanceof WhitespaceCharPredicate; + } + + @Override + public int hashCode() { + return WhitespaceCharPredicate.class.hashCode(); + } + + @Override + public String toString() { + return "WhitespaceCharPredicate"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/primitive/WordCharPredicate.java b/petitparser-core/src/main/java/org/petitparser/parser/primitive/WordCharPredicate.java new file mode 100644 index 0000000..e0b83d4 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/primitive/WordCharPredicate.java @@ -0,0 +1,39 @@ +package org.petitparser.parser.primitive; + +/** + * Character predicate matching word characters (letters or digits). + */ +public class WordCharPredicate implements CharacterPredicate { + + public static final WordCharPredicate INSTANCE = new WordCharPredicate(); + + @Override + public boolean test(char value) { + return Character.isLetterOrDigit(value); + } + + @Override + public boolean test(int value) { + return Character.isLetterOrDigit(value); + } + + @Override + public boolean isEqualTo(CharacterPredicate other) { + return other instanceof WordCharPredicate; + } + + @Override + public boolean equals(Object other) { + return other instanceof WordCharPredicate; + } + + @Override + public int hashCode() { + return WordCharPredicate.class.hashCode(); + } + + @Override + public String toString() { + return "WordCharPredicate"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/repeating/FoldFunction.java b/petitparser-core/src/main/java/org/petitparser/parser/repeating/FoldFunction.java new file mode 100644 index 0000000..25751a2 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/repeating/FoldFunction.java @@ -0,0 +1,21 @@ +package org.petitparser.parser.repeating; + +/** + * A callback for folding elements of a {@link SeparatedList} with their separators. + * + * @param the element type. + * @param the separator type. + */ +@FunctionalInterface +public interface FoldFunction { + + /** + * Combines {@code left}, {@code separator}, and {@code right} into a new value. + * + * @param left the left-hand element. + * @param separator the separator between elements. + * @param right the right-hand element. + * @return the combined result. + */ + R apply(R left, S separator, R right); +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/repeating/GreedyRepeatingParser.java b/petitparser-core/src/main/java/org/petitparser/parser/repeating/GreedyRepeatingParser.java index a26afc7..30efd9c 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/repeating/GreedyRepeatingParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/repeating/GreedyRepeatingParser.java @@ -101,4 +101,3 @@ public GreedyRepeatingParser copy() { return new GreedyRepeatingParser(delegate, limit, min, max); } } - diff --git a/petitparser-core/src/main/java/org/petitparser/parser/repeating/RepeatingCharacterParser.java b/petitparser-core/src/main/java/org/petitparser/parser/repeating/RepeatingCharacterParser.java new file mode 100644 index 0000000..8cc824b --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/repeating/RepeatingCharacterParser.java @@ -0,0 +1,231 @@ +package org.petitparser.parser.repeating; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterPredicate; + +import java.util.Objects; + +/** + * A parser that repeatedly parses between 'min' and 'max' instances of a character predicate + * and returns the consumed range as a {@link String}. + */ +public class RepeatingCharacterParser extends Parser { + + protected final CharacterPredicate predicate; + protected final String message; + protected final int min; + protected final int max; + protected final boolean unicode; + + /** + * Constructs a repeating character parser. + * + * @param predicate the predicate matching each character. + * @param message the failure message to report if min repetitions are not satisfied. + * @param min the minimum number of repetitions. + * @param max the maximum number of repetitions, or {@link RepeatingParser#UNBOUNDED}. + */ + public RepeatingCharacterParser( + CharacterPredicate predicate, String message, int min, int max) { + this(predicate, message, min, max, false); + } + + /** + * Constructs a repeating character parser with Unicode code point support. + */ + public RepeatingCharacterParser( + CharacterPredicate predicate, String message, int min, int max, boolean unicode) { + this.predicate = Objects.requireNonNull(predicate, "Undefined predicate"); + this.message = Objects.requireNonNull(message, "Undefined message"); + this.min = min; + this.max = max; + this.unicode = unicode; + if (min < 0) { + throw new IllegalArgumentException("Invalid min repetitions: " + getRange()); + } + if (max != RepeatingParser.UNBOUNDED && min > max) { + throw new IllegalArgumentException("Invalid max repetitions: " + getRange()); + } + } + + public CharacterPredicate getPredicate() { + return predicate; + } + + public String getMessage() { + return message; + } + + public int getMin() { + return min; + } + + public int getMax() { + return max; + } + + public boolean isUnicode() { + return unicode; + } + + @Override + public Result parseOn(Context context) { + String buffer = context.getBuffer(); + int position = context.getPosition(); + int end = buffer.length(); + int current = position; + int count = 0; + if (unicode) { + while (count < min) { + if (current >= end) { + return context.failure(message, current); + } + char ch = buffer.charAt(current); + int next = current + 1; + int codePoint = ch; + if (Character.isHighSurrogate(ch) && next < end) { + char nextCh = buffer.charAt(next); + if (Character.isLowSurrogate(nextCh)) { + codePoint = Character.toCodePoint(ch, nextCh); + next++; + } + } + if (!predicate.test(codePoint)) { + return context.failure(message, current); + } + current = next; + count++; + } + while (max == RepeatingParser.UNBOUNDED || count < max) { + if (current >= end) { + break; + } + char ch = buffer.charAt(current); + int next = current + 1; + int codePoint = ch; + if (Character.isHighSurrogate(ch) && next < end) { + char nextCh = buffer.charAt(next); + if (Character.isLowSurrogate(nextCh)) { + codePoint = Character.toCodePoint(ch, nextCh); + next++; + } + } + if (!predicate.test(codePoint)) { + break; + } + current = next; + count++; + } + return context.success(buffer.substring(position, current), current); + } else { + while (count < min) { + if (current >= end || !predicate.test(buffer.charAt(current))) { + return context.failure(message, current); + } + current++; + count++; + } + while (max == RepeatingParser.UNBOUNDED || count < max) { + if (current >= end || !predicate.test(buffer.charAt(current))) { + break; + } + current++; + count++; + } + return context.success(buffer.substring(position, current), current); + } + } + + @Override + public int fastParseOn(String buffer, int position) { + int end = buffer.length(); + int current = position; + int count = 0; + if (unicode) { + while (count < min) { + if (current >= end) { + return -1; + } + char ch = buffer.charAt(current); + int next = current + 1; + int codePoint = ch; + if (Character.isHighSurrogate(ch) && next < end) { + char nextCh = buffer.charAt(next); + if (Character.isLowSurrogate(nextCh)) { + codePoint = Character.toCodePoint(ch, nextCh); + next++; + } + } + if (!predicate.test(codePoint)) { + return -1; + } + current = next; + count++; + } + while (max == RepeatingParser.UNBOUNDED || count < max) { + if (current >= end) { + break; + } + char ch = buffer.charAt(current); + int next = current + 1; + int codePoint = ch; + if (Character.isHighSurrogate(ch) && next < end) { + char nextCh = buffer.charAt(next); + if (Character.isLowSurrogate(nextCh)) { + codePoint = Character.toCodePoint(ch, nextCh); + next++; + } + } + if (!predicate.test(codePoint)) { + break; + } + current = next; + count++; + } + return current; + } else { + while (count < min) { + if (current >= end || !predicate.test(buffer.charAt(current))) { + return -1; + } + current++; + count++; + } + while (max == RepeatingParser.UNBOUNDED || count < max) { + if (current >= end || !predicate.test(buffer.charAt(current))) { + break; + } + current++; + count++; + } + return current; + } + } + + @Override + public RepeatingCharacterParser copy() { + return new RepeatingCharacterParser(predicate, message, min, max, unicode); + } + + @Override + protected boolean hasEqualProperties(Parser other) { + RepeatingCharacterParser that = (RepeatingCharacterParser) other; + return super.hasEqualProperties(other) && + Objects.equals(predicate, that.predicate) && + Objects.equals(message, that.message) && + min == that.min && + max == that.max && + unicode == that.unicode; + } + + @Override + public String toString() { + return super.toString() + "[" + message + ", " + getRange() + "]"; + } + + private String getRange() { + return min + ".." + (max == RepeatingParser.UNBOUNDED ? "*" : max); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/repeating/RepeatingParser.java b/petitparser-core/src/main/java/org/petitparser/parser/repeating/RepeatingParser.java index 6fea055..708ef76 100644 --- a/petitparser-core/src/main/java/org/petitparser/parser/repeating/RepeatingParser.java +++ b/petitparser-core/src/main/java/org/petitparser/parser/repeating/RepeatingParser.java @@ -42,6 +42,20 @@ public String toString() { return super.toString() + "[" + getRange() + "]"; } + /** + * Returns the minimum number of repetitions. + */ + public int getMin() { + return min; + } + + /** + * Returns the maximum number of repetitions, or {@link #UNBOUNDED}. + */ + public int getMax() { + return max; + } + private String getRange() { return min + ".." + (max == UNBOUNDED ? "*" : max); } diff --git a/petitparser-core/src/main/java/org/petitparser/parser/repeating/SeparatedList.java b/petitparser-core/src/main/java/org/petitparser/parser/repeating/SeparatedList.java new file mode 100644 index 0000000..e8529ee --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/repeating/SeparatedList.java @@ -0,0 +1,186 @@ +package org.petitparser.parser.repeating; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * An immutable data structure holding parsed elements and their intervening separators. + * + * @param the element type. + * @param the separator type. + */ +public class SeparatedList implements Iterable { + + private final List elements; + private final List separators; + + /** + * Constructs an empty separated list. + */ + public SeparatedList() { + this(Collections.emptyList(), Collections.emptyList()); + } + + /** + * Constructs a separated list with elements and no separators. + * + * @param elements the parsed elements. + * @throws IllegalArgumentException if {@code elements} contains more than one element. + */ + public SeparatedList(List elements) { + this(elements, Collections.emptyList()); + } + + /** + * Constructs an immutable separated list with the given elements and separators. + * + * @param elements the parsed elements. + * @param separators the parsed separators. + * @throws IllegalArgumentException if the number of separators is inconsistent with the number of elements. + */ + public SeparatedList(List elements, List separators) { + this.elements = Collections.unmodifiableList( + new ArrayList<>(Objects.requireNonNull(elements, "Undefined elements"))); + this.separators = Collections.unmodifiableList( + new ArrayList<>(Objects.requireNonNull(separators, "Undefined separators"))); + if (Math.max(0, this.elements.size() - 1) != this.separators.size()) { + throw new IllegalArgumentException( + "Inconsistent number of elements (" + this.elements.size() + + ") and separators (" + this.separators.size() + ")"); + } + } + + /** + * Returns the unmodifiable list of elements. + */ + public List getElements() { + return elements; + } + + /** + * Returns the unmodifiable list of separators. + */ + public List getSeparators() { + return separators; + } + + /** + * Returns an iterable over the elements and interleaved separators in order of appearance. + */ + public Iterable getSequential() { + return this; + } + + /** + * Returns an unmodifiable list of the elements and interleaved separators in order of appearance. + */ + public List getSequentialList() { + List result = new ArrayList<>(elements.size() + separators.size()); + for (int i = 0; i < elements.size(); i++) { + result.add(elements.get(i)); + if (i < separators.size()) { + result.add(separators.get(i)); + } + } + return Collections.unmodifiableList(result); + } + + /** + * Combines the elements by grouping from the left and calling {@code callback} on all consecutive + * elements with the corresponding separator. + * + * @param callback the folding function. + * @return the folded result. + * @throws NoSuchElementException if the separated list has no elements. + */ + public R foldLeft(FoldFunction callback) { + Objects.requireNonNull(callback, "Undefined callback"); + if (elements.isEmpty()) { + throw new NoSuchElementException("Cannot fold an empty SeparatedList"); + } + R result = elements.get(0); + for (int i = 1; i < elements.size(); i++) { + result = callback.apply(result, separators.get(i - 1), elements.get(i)); + } + return result; + } + + /** + * Combines the elements by grouping from the right and calling {@code callback} on all consecutive + * elements with the corresponding separator. + * + * @param callback the folding function. + * @return the folded result. + * @throws NoSuchElementException if the separated list has no elements. + */ + public R foldRight(FoldFunction callback) { + Objects.requireNonNull(callback, "Undefined callback"); + if (elements.isEmpty()) { + throw new NoSuchElementException("Cannot fold an empty SeparatedList"); + } + R result = elements.get(elements.size() - 1); + for (int i = elements.size() - 2; i >= 0; i--) { + result = callback.apply(elements.get(i), separators.get(i), result); + } + return result; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private int index = 0; + + @Override + public boolean hasNext() { + return index < elements.size() + separators.size(); + } + + @Override + public Object next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + int i = index++; + return (i % 2 == 0) ? elements.get(i / 2) : separators.get(i / 2); + } + }; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + SeparatedList that = (SeparatedList) other; + return Objects.equals(elements, that.elements) && + Objects.equals(separators, that.separators); + } + + @Override + public int hashCode() { + return Objects.hash(elements, separators); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("("); + boolean first = true; + for (Object item : this) { + if (!first) { + sb.append(", "); + } + sb.append(item); + first = false; + } + sb.append(")"); + return sb.toString(); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/parser/repeating/SeparatedRepeatingParser.java b/petitparser-core/src/main/java/org/petitparser/parser/repeating/SeparatedRepeatingParser.java new file mode 100644 index 0000000..6a844c4 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/parser/repeating/SeparatedRepeatingParser.java @@ -0,0 +1,148 @@ +package org.petitparser.parser.repeating; + +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.SequentialParser; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * A parser that repeatedly parses between 'min' and 'max' instances of its delegate + * separated by a separator parser, returning a {@link SeparatedList}. + */ +public class SeparatedRepeatingParser extends RepeatingParser implements SequentialParser { + + protected Parser separator; + + /** + * Constructs a separated repeating parser. + * + * @param delegate the parser matching the elements. + * @param separator the parser matching the separator. + * @param min the minimum number of elements. + * @param max the maximum number of elements, or {@link RepeatingParser#UNBOUNDED}. + */ + public SeparatedRepeatingParser( + Parser delegate, Parser separator, int min, int max) { + super(delegate, min, max); + this.separator = Objects.requireNonNull(separator, "Undefined separator"); + } + + public Parser getSeparator() { + return separator; + } + + public void setSeparator(Parser separator) { + this.separator = Objects.requireNonNull(separator, "Undefined separator"); + } + + @Override + public Result parseOn(Context context) { + Context current = context; + List elements = new ArrayList<>(); + List separators = new ArrayList<>(); + + while (elements.size() < min) { + if (!elements.isEmpty()) { + Result separation = separator.parseOn(current); + if (separation.isFailure()) { + return separation; + } + current = separation; + separators.add(separation.get()); + } + Result result = delegate.parseOn(current); + if (result.isFailure()) { + return result; + } + current = result; + elements.add(result.get()); + } + + while (max == UNBOUNDED || elements.size() < max) { + Context previous = current; + if (!elements.isEmpty()) { + Result separation = separator.parseOn(current); + if (separation.isFailure()) { + break; + } + current = separation; + separators.add(separation.get()); + } + Result result = delegate.parseOn(current); + if (result.isFailure()) { + if (!elements.isEmpty()) { + separators.remove(separators.size() - 1); + } + return previous.success(new SeparatedList<>(elements, separators)); + } + current = result; + elements.add(result.get()); + } + + return current.success(new SeparatedList<>(elements, separators)); + } + + @Override + public int fastParseOn(String buffer, int position) { + int count = 0; + int current = position; + + while (count < min) { + if (count > 0) { + int separation = separator.fastParseOn(buffer, current); + if (separation < 0) { + return -1; + } + current = separation; + } + int result = delegate.fastParseOn(buffer, current); + if (result < 0) { + return -1; + } + count++; + current = result; + } + + while (max == UNBOUNDED || count < max) { + int previous = current; + if (count > 0) { + int separation = separator.fastParseOn(buffer, current); + if (separation < 0) { + break; + } + current = separation; + } + int result = delegate.fastParseOn(buffer, current); + if (result < 0) { + return previous; + } + count++; + current = result; + } + + return current; + } + + @Override + public List getChildren() { + return Arrays.asList(delegate, separator); + } + + @Override + public void replace(Parser source, Parser target) { + super.replace(source, target); + if (separator == source) { + separator = target; + } + } + + @Override + public SeparatedRepeatingParser copy() { + return new SeparatedRepeatingParser(delegate, separator, min, max); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/tools/ExpressionBuilder.java b/petitparser-core/src/main/java/org/petitparser/tools/ExpressionBuilder.java index 92fff6e..126f14f 100644 --- a/petitparser-core/src/main/java/org/petitparser/tools/ExpressionBuilder.java +++ b/petitparser-core/src/main/java/org/petitparser/tools/ExpressionBuilder.java @@ -5,27 +5,69 @@ import org.petitparser.parser.combinators.SequenceParser; import org.petitparser.parser.combinators.SettableParser; import org.petitparser.parser.primitive.FailureParser; +import org.petitparser.parser.repeating.SeparatedList; +import org.petitparser.parser.repeating.SeparatedRepeatingParser; +import org.petitparser.utils.functions.Function3; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; import java.util.function.Function; /** * A builder that allows the simple definition of expression grammars with * prefix, postfix, and left- and right-associative infix operators. + * + * @param the type of parsed expression values */ -public class ExpressionBuilder { +public class ExpressionBuilder { private final SettableParser loopback = SettableParser.undefined(); - private final List groups = new ArrayList<>(); + private final List groups = new ArrayList<>(); + private final List primitives = new ArrayList<>(); + + /** + * Returns the loopback parser for this expression builder. + * Can be used to loop back to this parser in recursive constructs. + */ + public SettableParser getLoopback() { + return loopback; + } + + /** + * Returns the loopback parser for this expression builder. + * Can be used to loop back to this parser in recursive constructs. + */ + public SettableParser loopback() { + return loopback; + } + + /** + * Defines a new primitive, literal, or value {@code parser}. + */ + public ExpressionBuilder primitive(Parser parser) { + return primitive(parser, (Function) null); + } + + /** + * Defines a new primitive, literal, or value {@code parser}. Evaluates the optional + * {@code action} with the parsed {@code value}. + */ + @SuppressWarnings("unchecked") + public ExpressionBuilder primitive( + Parser parser, Function action) { + Objects.requireNonNull(parser, "Undefined parser"); + primitives.add(action == null ? parser : parser.map((Function) action)); + return this; + } /** * Creates a new group of operators that share the same priority. */ - public ExpressionGroup group() { - ExpressionGroup group = new ExpressionGroup(); + public ExpressionGroup group() { + ExpressionGroup group = new ExpressionGroup<>(loopback); groups.add(group); return group; } @@ -34,31 +76,193 @@ public ExpressionGroup group() { * Builds the expression parser. */ public Parser build() { - Parser parser = FailureParser.withMessage( - "Highest priority group should define a primitive parser."); - for (ExpressionGroup group : groups) { + Parser parser = primitives.isEmpty() + ? FailureParser.withMessage("Highest priority group should define a primitive parser.") + : buildChoice(primitives); + for (ExpressionGroupBase group : groups) { parser = group.build(parser); } loopback.set(parser); return parser; } + static Parser buildChoice(List parsers) { + return buildChoice(parsers, null); + } + + static Parser buildChoice(List parsers, Parser otherwise) { + if (parsers.isEmpty()) { + return otherwise; + } else if (parsers.size() == 1) { + return parsers.get(0); + } else { + return new ChoiceParser(parsers.toArray(new Parser[0])); + } + } + /** * Models a group of operators of the same precedence. + * + * @param the type of parsed expression values + */ + public static class ExpressionGroup extends ExpressionGroupBase { + + public ExpressionGroup() { + super(); + } + + public ExpressionGroup(SettableParser loopback) { + super(loopback); + } + + @Override + public ExpressionGroup primitive(Parser parser) { + super.primitive(parser); + return this; + } + + @Override + public ExpressionGroup wrapper(Parser left, Parser right) { + super.wrapper(left, right); + return this; + } + + /** + * Defines a new wrapper using {@code left} and {@code right} parsers. + * Evaluates the typed {@code action} with the parsed {@code left}, {@code value}, and {@code right}. + */ + @SuppressWarnings("unchecked") + public ExpressionGroup wrapper( + Parser left, Parser right, Function3 action) { + Objects.requireNonNull(left, "Undefined left parser"); + Objects.requireNonNull(right, "Undefined right parser"); + Objects.requireNonNull(action, "Undefined action"); + Parser parser = new SequenceParser(left, loopback, right); + wrappers.add(parser.map((List tuple) -> + action.apply((L) tuple.get(0), (T) tuple.get(1), (R) tuple.get(2)))); + return this; + } + + @Override + public ExpressionGroup prefix(Parser parser) { + super.prefix(parser); + return this; + } + + /** + * Adds a prefix operator {@code parser}. Evaluates the typed {@code + * action} with the parsed {@code operator} and {@code value}. + */ + @SuppressWarnings("unchecked") + public ExpressionGroup prefix( + Parser parser, BiFunction action) { + Objects.requireNonNull(parser, "Undefined parser"); + Objects.requireNonNull(action, "Undefined action"); + PrefixAction prefixAction = (op, val) -> action.apply((O) op, (T) val); + prefix.add(parser.map(operator -> new ExpressionResultPrefix(operator, prefixAction))); + return this; + } + + @Override + public ExpressionGroup postfix(Parser parser) { + super.postfix(parser); + return this; + } + + /** + * Adds a postfix operator {@code parser}. Evaluates the typed {@code + * action} with the parsed {@code value} and {@code operator}. + */ + @SuppressWarnings("unchecked") + public ExpressionGroup postfix( + Parser parser, BiFunction action) { + Objects.requireNonNull(parser, "Undefined parser"); + Objects.requireNonNull(action, "Undefined action"); + PostfixAction postfixAction = (val, op) -> action.apply((T) val, (O) op); + postfix.add(parser.map(operator -> new ExpressionResultPostfix(operator, postfixAction))); + return this; + } + + @Override + public ExpressionGroup right(Parser parser) { + super.right(parser); + return this; + } + + /** + * Adds a right-associative operator {@code parser}. Evaluates the typed + * {@code action} with the parsed {@code left} term, {@code operator}, and + * {@code right} term. + */ + @SuppressWarnings("unchecked") + public ExpressionGroup right( + Parser parser, Function3 action) { + Objects.requireNonNull(parser, "Undefined parser"); + Objects.requireNonNull(action, "Undefined action"); + InfixAction infixAction = (l, op, r) -> action.apply((T) l, (O) op, (T) r); + right.add(parser.map(operator -> new ExpressionResultInfix(operator, infixAction))); + return this; + } + + @Override + public ExpressionGroup left(Parser parser) { + super.left(parser); + return this; + } + + /** + * Adds a left-associative operator {@code parser}. Evaluates the typed + * {@code action} with the parsed {@code left} term, {@code operator}, and + * {@code right} term. + */ + @SuppressWarnings("unchecked") + public ExpressionGroup left( + Parser parser, Function3 action) { + Objects.requireNonNull(parser, "Undefined parser"); + Objects.requireNonNull(action, "Undefined action"); + InfixAction infixAction = (l, op, r) -> action.apply((T) l, (O) op, (T) r); + left.add(parser.map(operator -> new ExpressionResultInfix(operator, infixAction))); + return this; + } + + /** + * Makes the group optional and instead returns the provided {@code value}. + */ + @Override + public ExpressionGroup optional(Object value) { + super.optional(value); + return this; + } + } + + /** + * Base class for expression groups providing legacy untyped and method-level generic operator definitions. + * Being non-generic, it ensures raw-type usage does not undergo method type erasure. */ - public class ExpressionGroup { + public static class ExpressionGroupBase { + + final SettableParser loopback; + final List primitives = new ArrayList<>(); + final List wrappers = new ArrayList<>(); + final List prefix = new ArrayList<>(); + final List postfix = new ArrayList<>(); + final List right = new ArrayList<>(); + final List left = new ArrayList<>(); + private Object optionalValue; + private boolean hasOptional = false; + + public ExpressionGroupBase() { + this(SettableParser.undefined()); + } - private final List primitives = new ArrayList<>(); - private final List wrappers = new ArrayList<>(); - private final List prefix = new ArrayList<>(); - private final List postfix = new ArrayList<>(); - private final List right = new ArrayList<>(); - private final List left = new ArrayList<>(); + public ExpressionGroupBase(SettableParser loopback) { + this.loopback = Objects.requireNonNull(loopback, "Undefined loopback"); + } /** * Defines a new primitive or literal {@code parser}. */ - public ExpressionGroup primitive(Parser parser) { + public ExpressionGroupBase primitive(Parser parser) { return primitive(parser, null); } @@ -66,36 +270,53 @@ public ExpressionGroup primitive(Parser parser) { * Defines a new primitive or literal {@code parser}. Evaluates the optional * {@code action} with the parsed {@code value}. */ - public ExpressionGroup primitive( - Parser parser, /*@Nullable*/ Function action) { - primitives.add(action == null ? parser : parser.map(action)); + @SuppressWarnings("unchecked") + public ExpressionGroupBase primitive( + Parser parser, Function action) { + Objects.requireNonNull(parser, "Undefined parser"); + primitives.add(action == null ? parser : parser.map((Function) action)); return this; } private Parser buildPrimitive(Parser inner) { - return buildChoice(primitives, inner); + if (primitives.isEmpty()) { + return inner; + } + List choices = new ArrayList<>(primitives); + if (inner != null && !(inner instanceof FailureParser)) { + choices.add(inner); + } + return buildChoice(choices, inner); } /** * Defines a new wrapper using {@code left} and {@code right} parsers. */ - public ExpressionGroup wrapper(Parser left, Parser right) { - return wrapper(left, right, null); + public ExpressionGroupBase wrapper(Parser left, Parser right) { + Objects.requireNonNull(left, "Undefined left parser"); + Objects.requireNonNull(right, "Undefined right parser"); + wrappers.add(new SequenceParser(left, loopback, right)); + return this; } /** * Defines a new wrapper using {@code left} and {@code right} parsers. - * Evaluates the optional {@code action} with the parsed {@code left}, - * {@code value} and {@code right}. + * Evaluates the optional {@code action} with the parsed list of {@code [left, value, right]}. */ - public ExpressionGroup wrapper( - Parser left, Parser right, /*@Nullable*/ Function action) { + @SuppressWarnings("unchecked") + public ExpressionGroupBase wrapper( + Parser left, Parser right, Function action) { + Objects.requireNonNull(left, "Undefined left parser"); + Objects.requireNonNull(right, "Undefined right parser"); Parser parser = new SequenceParser(left, loopback, right); - wrappers.add(action == null ? parser : parser.map(action)); + wrappers.add(action == null ? parser : parser.map((Function) action)); return this; } private Parser buildWrapper(Parser inner) { + if (wrappers.isEmpty()) { + return inner; + } List choices = new ArrayList<>(wrappers); choices.add(inner); return buildChoice(choices, inner); @@ -104,31 +325,36 @@ private Parser buildWrapper(Parser inner) { /** * Adds a prefix operator {@code parser}. */ - public ExpressionGroup prefix(Parser parser) { - return prefix(parser, null); + public ExpressionGroupBase prefix(Parser parser) { + return prefix(parser, (Function, Object>) null); } /** * Adds a prefix operator {@code parser}. Evaluates the optional {@code - * action} with the parsed {@code operator} and {@code value}. + * action} with the parsed list of {@code [operator, value]}. */ - public ExpressionGroup prefix( - Parser parser, /*@Nullable*/ Function action) { - addTo(prefix, parser, action); + @SuppressWarnings("unchecked") + public ExpressionGroupBase prefix( + Parser parser, Function action) { + Objects.requireNonNull(parser, "Undefined parser"); + PrefixAction prefixAction = action == null + ? (op, val) -> Arrays.asList(op, val) + : (op, val) -> action.apply((T) Arrays.asList(op, val)); + prefix.add(parser.map(operator -> new ExpressionResultPrefix(operator, prefixAction))); return this; } + @SuppressWarnings("unchecked") private Parser buildPrefix(Parser inner) { if (prefix.isEmpty()) { return inner; } else { Parser sequence = new SequenceParser(buildChoice(prefix).star(), inner); - return sequence.map((List> tuple) -> { + return sequence.map((List tuple) -> { Object value = tuple.get(1); - List tuples = tuple.get(0); - Collections.reverse(tuples); - for (ExpressionResult result : tuples) { - value = result.action.apply(Arrays.asList(result.operator, value)); + List prefixes = (List) tuple.get(0); + for (int i = prefixes.size() - 1; i >= 0; i--) { + value = prefixes.get(i).apply(value); } return value; }); @@ -138,30 +364,36 @@ private Parser buildPrefix(Parser inner) { /** * Adds a postfix operator {@code parser}. */ - public ExpressionGroup postfix(Parser parser) { - return postfix(parser, null); + public ExpressionGroupBase postfix(Parser parser) { + return postfix(parser, (Function, Object>) null); } /** * Adds a postfix operator {@code parser}. Evaluates the optional {@code - * action} with the parsed {@code value} and {@code operator}. + * action} with the parsed list of {@code [value, operator]}. */ - public ExpressionGroup postfix( - Parser parser, /*@Nullable*/ Function action) { - addTo(postfix, parser, action); + @SuppressWarnings("unchecked") + public ExpressionGroupBase postfix( + Parser parser, Function action) { + Objects.requireNonNull(parser, "Undefined parser"); + PostfixAction postfixAction = action == null + ? (val, op) -> Arrays.asList(val, op) + : (val, op) -> action.apply((T) Arrays.asList(val, op)); + postfix.add(parser.map(operator -> new ExpressionResultPostfix(operator, postfixAction))); return this; } + @SuppressWarnings("unchecked") private Parser buildPostfix(Parser inner) { if (postfix.isEmpty()) { return inner; } else { - Parser sequence = - new SequenceParser(inner, buildChoice(postfix).star()); - return sequence.map((List> tuple) -> { + Parser sequence = new SequenceParser(inner, buildChoice(postfix).star()); + return sequence.map((List tuple) -> { Object value = tuple.get(0); - for (ExpressionResult result : tuple.get(1)) { - value = result.action.apply(Arrays.asList(value, result.operator)); + List postfixes = (List) tuple.get(1); + for (int i = 0; i < postfixes.size(); i++) { + value = postfixes.get(i).apply(value); } return value; }); @@ -171,18 +403,22 @@ private Parser buildPostfix(Parser inner) { /** * Adds a right-associative operator {@code parser}. */ - public ExpressionGroup right(Parser parser) { - return right(parser, null); + public ExpressionGroupBase right(Parser parser) { + return right(parser, (Function, Object>) null); } /** * Adds a right-associative operator {@code parser}. Evaluates the optional - * {@code action} with the parsed {@code left} term, {@code operator}, and - * {@code right} term. + * {@code action} with the parsed list of {@code [left, operator, right]}. */ - public ExpressionGroup right( - Parser parser, /*@Nullable*/ Function action) { - addTo(right, parser, action); + @SuppressWarnings("unchecked") + public ExpressionGroupBase right( + Parser parser, Function action) { + Objects.requireNonNull(parser, "Undefined parser"); + InfixAction infixAction = action == null + ? (l, op, r) -> Arrays.asList(l, op, r) + : (l, op, r) -> action.apply((T) Arrays.asList(l, op, r)); + right.add(parser.map(operator -> new ExpressionResultInfix(operator, infixAction))); return this; } @@ -190,36 +426,31 @@ private Parser buildRight(Parser inner) { if (right.isEmpty()) { return inner; } else { - Parser sequence = inner.separatedBy(buildChoice(right)); - return sequence.map((List innerSequence) -> { - Object result = innerSequence.get(innerSequence.size() - 1); - for (int i = innerSequence.size() - 2; i > 0; i -= 2) { - ExpressionResult expressionResult = - (ExpressionResult) innerSequence.get(i); - result = expressionResult.action.apply(Arrays - .asList(innerSequence.get(i - 1), expressionResult.operator, - result)); - } - return result; - }); + SeparatedRepeatingParser sequence = inner.plusSeparated(buildChoice(right)); + return sequence.map((SeparatedList sequenceList) -> + sequenceList.foldRight((left, opResult, right) -> opResult.apply(left, right))); } } /** * Adds a left-associative operator {@code parser}. */ - public ExpressionGroup left(Parser parser) { - return left(parser, null); + public ExpressionGroupBase left(Parser parser) { + return left(parser, (Function, Object>) null); } /** * Adds a left-associative operator {@code parser}. Evaluates the optional - * {@code action} with the parsed {@code left} term, {@code operator}, and - * {@code right} term. + * {@code action} with the parsed list of {@code [left, operator, right]}. */ - public ExpressionGroup left( - Parser parser, /*@Nullable*/ Function action) { - addTo(left, parser, action); + @SuppressWarnings("unchecked") + public ExpressionGroupBase left( + Parser parser, Function action) { + Objects.requireNonNull(parser, "Undefined parser"); + InfixAction infixAction = action == null + ? (l, op, r) -> Arrays.asList(l, op, r) + : (l, op, r) -> action.apply((T) Arrays.asList(l, op, r)); + left.add(parser.map(operator -> new ExpressionResultInfix(operator, infixAction))); return this; } @@ -227,60 +458,89 @@ private Parser buildLeft(Parser inner) { if (left.isEmpty()) { return inner; } else { - Parser sequence = inner.separatedBy(buildChoice(left)); - return sequence.map((List innerSequence) -> { - Object result = innerSequence.get(0); - for (int i = 1; i < innerSequence.size(); i += 2) { - ExpressionResult expressionResult = - (ExpressionResult) innerSequence.get(i); - result = expressionResult.action.apply(Arrays - .asList(result, expressionResult.operator, - innerSequence.get(i + 1))); - } - return result; - }); + SeparatedRepeatingParser sequence = inner.plusSeparated(buildChoice(left)); + return sequence.map((SeparatedList sequenceList) -> + sequenceList.foldLeft((left, opResult, right) -> opResult.apply(left, right))); } } - // helper to connect operator parser and action, and add to list - @SuppressWarnings("unchecked") - private void addTo( - List list, Parser parser, /*@Nullable*/ Function action) { - Function mapper = action == null ? Function.identity() : - (Function) action; - list.add(parser.map(operator -> new ExpressionResult(operator, mapper))); + /** + * Makes the group optional and instead returns the provided {@code value}. + */ + public ExpressionGroupBase optional(Object value) { + if (hasOptional) { + throw new IllegalStateException("At most one optional value expected"); + } + this.optionalValue = value; + this.hasOptional = true; + return this; } - // helper to build an optimal choice parser - private Parser buildChoice(List parsers) { - return buildChoice(parsers, null); + private Parser buildOptional(Parser inner) { + return hasOptional ? inner.optional(optionalValue) : inner; } - private Parser buildChoice(List parsers, Parser otherwise) { - if (parsers.isEmpty()) { - return otherwise; - } else if (parsers.size() == 1) { - return parsers.get(0); - } else { - return new ChoiceParser(parsers.toArray(new Parser[parsers.size()])); - } + // helper to build the group of parsers + Parser build(Parser inner) { + return buildOptional(buildLeft(buildRight( + buildPostfix(buildPrefix(buildWrapper(buildPrimitive(inner))))))); } + } - // helper to build the group of parsers - private Parser build(Parser inner) { - return buildLeft(buildRight( - buildPostfix(buildPrefix(buildWrapper(buildPrimitive(inner)))))); + @FunctionalInterface + interface PrefixAction { + Object apply(Object operator, Object value); + } + + static class ExpressionResultPrefix { + final Object operator; + final PrefixAction action; + + ExpressionResultPrefix(Object operator, PrefixAction action) { + this.operator = operator; + this.action = action; + } + + Object apply(Object value) { + return action.apply(operator, value); + } + } + + @FunctionalInterface + interface PostfixAction { + Object apply(Object value, Object operator); + } + + static class ExpressionResultPostfix { + final Object operator; + final PostfixAction action; + + ExpressionResultPostfix(Object operator, PostfixAction action) { + this.operator = operator; + this.action = action; + } + + Object apply(Object value) { + return action.apply(value, operator); } } - // helper class to associate operators and actions - private static class ExpressionResult { + @FunctionalInterface + interface InfixAction { + Object apply(Object left, Object operator, Object right); + } + + static class ExpressionResultInfix { final Object operator; - final Function action; + final InfixAction action; - private ExpressionResult(Object operator, Function action) { + ExpressionResultInfix(Object operator, InfixAction action) { this.operator = operator; this.action = action; } + + Object apply(Object left, Object right) { + return action.apply(left, operator, right); + } } } diff --git a/petitparser-core/src/main/java/org/petitparser/tools/GrammarDefinition.java b/petitparser-core/src/main/java/org/petitparser/tools/GrammarDefinition.java index 16dcd63..e38d66a 100644 --- a/petitparser-core/src/main/java/org/petitparser/tools/GrammarDefinition.java +++ b/petitparser-core/src/main/java/org/petitparser/tools/GrammarDefinition.java @@ -36,6 +36,13 @@ public class GrammarDefinition { private final Map parsers = new HashMap<>(); + /** + * Creates a typed production key with the given {@code name}. + */ + public static Production production(String name) { + return Production.of(name); + } + /** * Returns a reference to the production with the given {@code name}. */ @@ -43,6 +50,13 @@ protected final Parser ref(String name) { return new Reference(name); } + /** + * Returns a reference to the given typed {@code production}. + */ + protected final Parser ref(Production production) { + return new Reference(production.getName()); + } + /** * Defines a production with a {@code name} and a {@code parser}. */ @@ -53,6 +67,13 @@ protected final void def(String name, Parser parser) { parsers.put(Objects.requireNonNull(name), Objects.requireNonNull(parser)); } + /** + * Defines a typed production with a {@code production} key and a {@code parser}. + */ + protected final void def(Production production, Parser parser) { + def(production.getName(), parser); + } + /** * Redefines an existing production with a {@code name} and a new {@code * parser}. @@ -64,6 +85,13 @@ protected final void redef(String name, Parser parser) { parsers.put(Objects.requireNonNull(name), Objects.requireNonNull(parser)); } + /** + * Redefines an existing typed production with a {@code production} key and a new {@code parser}. + */ + protected final void redef(Production production, Parser parser) { + redef(production.getName(), parser); + } + /** * Redefines an existing production with a {@code name} and a {@code function} * producing a new parser. Only call this method during initialization. @@ -75,6 +103,13 @@ protected final void redef(String name, Function function) { redef(name, function.apply(parsers.get(name))); } + /** + * Redefines an existing typed production with a {@code production} key and a {@code function}. + */ + protected final void redef(Production production, Function function) { + redef(production.getName(), function); + } + /** * Attaches an action {@code function} to an existing production {@code name}. * Only call this method during initialization. @@ -83,6 +118,13 @@ protected final void action(String name, Function function) { redef(name, parser -> parser.map(function)); } + /** + * Attaches an action {@code function} to an existing typed production {@code production}. + */ + protected final void action(Production production, Function function) { + action(production.getName(), function); + } + /** * Builds a parser starting from the production {@code "start"}. */ @@ -97,16 +139,23 @@ public Parser build(String name) { return resolve(new Reference(name)); } + /** + * Builds a parser starting from the provided typed {@code production}. + */ + public Parser build(Production production) { + return build(production.getName()); + } + private Parser resolve(Reference reference) { Map mapping = new HashMap<>(); List todo = new ArrayList<>(); todo.add(dereference(mapping, reference)); Set seen = new HashSet<>(todo); while (!todo.isEmpty()) { - Parser parent = todo.remove(todo.size() - 1); + Parser parent = todo.removeLast(); for (Parser child : parent.getChildren()) { - if (child instanceof Reference) { - Parser referenced = dereference(mapping, (Reference) child); + if (child instanceof Reference ref) { + Parser referenced = dereference(mapping, ref); parent.replace(child, referenced); child = referenced; } @@ -126,8 +175,7 @@ private Parser dereference( List references = new ArrayList<>(); references.add(reference); parser = reference.resolve(); - while (parser instanceof Reference) { - Reference otherReference = (Reference) parser; + while (parser instanceof Reference otherReference) { if (references.contains(otherReference)) { throw new IllegalStateException("Recursive references detected: " + String.join(", ", references.stream().map(ref -> ref.name) @@ -185,4 +233,4 @@ public int hashCode() { return name.hashCode(); } } -} \ No newline at end of file +} diff --git a/petitparser-core/src/main/java/org/petitparser/tools/GrammarParser.java b/petitparser-core/src/main/java/org/petitparser/tools/GrammarParser.java index 827fa70..542bdc7 100644 --- a/petitparser-core/src/main/java/org/petitparser/tools/GrammarParser.java +++ b/petitparser-core/src/main/java/org/petitparser/tools/GrammarParser.java @@ -15,8 +15,12 @@ public GrammarParser(GrammarDefinition definition, String name) { super(definition.build(name)); } + public GrammarParser(GrammarDefinition definition, Production production) { + super(definition.build(production)); + } + @Override public int fastParseOn(String buffer, int position) { return delegate.fastParseOn(buffer, position); } -} \ No newline at end of file +} diff --git a/petitparser-core/src/main/java/org/petitparser/tools/Indent.java b/petitparser-core/src/main/java/org/petitparser/tools/Indent.java new file mode 100644 index 0000000..e377d5d --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/tools/Indent.java @@ -0,0 +1,196 @@ +package org.petitparser.tools; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ChoiceParser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.utils.FailureJoiner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Stateful set of parsers to handle indentation-based grammars. + * + *

Typical use combines {@link #same()} to match the indentation of items at the current + * level and {@link #during(Parser)} to scope indented blocks: + * + *

{@code
+ * Indent indent = new Indent();
+ * Parser line = indent.same().seq(word().plus().flatten());
+ * Parser block = indent.during(line.plus());
+ * }
+ */ +public class Indent { + + private final Parser parser; + private final String message; + + final List stack = new ArrayList<>(); + String current = ""; + + private final Parser sameParser; + private final Parser increaseParser; + private final Parser decreaseParser; + + /** + * Creates an indentation handler with default whitespace parser and error message. + */ + public Indent() { + this(CharacterParser.pattern(" \t"), "indented expected"); + } + + /** + * Creates an indentation handler with a custom indentation {@code parser}. + * + * @param parser the parser used to read a single indentation character or sequence. + */ + public Indent(Parser parser) { + this(parser, "indented expected"); + } + + /** + * Creates an indentation handler with a default whitespace parser and custom error {@code message}. + * + * @param message the error message to use when an indentation is expected. + */ + public Indent(String message) { + this(CharacterParser.pattern(" \t"), message); + } + + /** + * Creates an indentation handler with a custom indentation {@code parser} and error {@code message}. + * + * @param parser the parser used to read a single indentation character or sequence. + * @param message the error message to use when an indentation is expected. + */ + public Indent(Parser parser, String message) { + this.parser = parser != null ? parser : CharacterParser.pattern(" \t"); + this.message = message != null ? message : "indented expected"; + + this.increaseParser = this.parser + .plusString(this.message) + .where((String value) -> { + if (value.startsWith(current) && value.length() > current.length()) { + stack.add(current); + current = value; + return true; + } else { + return false; + } + }) + .and(); + + this.sameParser = this.parser + .starString(this.message) + .where((String value) -> Objects.equals(value, current)); + + this.decreaseParser = Parser.epsilon().where(ignored -> { + if (!stack.isEmpty()) { + current = stack.remove(stack.size() - 1); + return true; + } else { + return false; + } + }); + } + + /** + * Returns the parser used to read a single indentation step. + */ + public Parser getParser() { + return parser; + } + + /** + * Returns the error message to use when an indentation is expected. + */ + public String getMessage() { + return message; + } + + /** + * Returns an unmodifiable view of the stack of parent indentations. + */ + public List getStack() { + return Collections.unmodifiableList(stack); + } + + /** + * Returns the currently active indentation string. + */ + public String getCurrent() { + return current; + } + + /** + * Parser that consumes and matches the current indentation level. + */ + public Parser same() { + return sameParser; + } + + /** + * Parser that increases the indentation. + * + *

The parser performs the following actions in sequence: + *

    + *
  1. verifies that the new indentation is deeper than the previous one,
  2. + *
  3. pushes the previous indentation to the stack,
  4. + *
  5. updates the current indentation with the new one, and
  6. + *
  7. returns the new indentation without consuming it.
  8. + *
+ * + *

Prefer using {@link #during(Parser)} instead to properly track the indentation state + * and ensure rollback on parse failure. + */ + public Parser increase() { + return increaseParser; + } + + /** + * Parser that decreases the indentation by one level. + * + *

Prefer using {@link #during(Parser)} instead to properly track the indentation state + * and ensure rollback on parse failure. + */ + public Parser decrease() { + return decreaseParser; + } + + /** + * Runs {@code inner} in a deeper indentation scope. + * + *

Before running {@code inner}, verifies that the next line has a deeper indentation + * than the current level and pushes the previous level onto the indentation stack. + * After {@code inner} succeeds, pops and restores the previous indentation level. + * + *

If {@code inner} fails, the indentation state is automatically rolled back to its state + * before entering the block, preserving the original parse failure position and message. + * This ensures that subsequent branches in choice combinators can backtrack safely without + * state corruption. + * + * @param inner the parser to run in an indented block. + * @return a parser that scopes indentation around {@code inner}. + */ + public Parser during(Parser inner) { + Objects.requireNonNull(inner, "Undefined inner parser"); + Parser rollback = Parser.failure().skip(decrease(), null); + Parser choice = new ChoiceParser(new FailureJoiner.SelectFirst(), inner, rollback); + return choice.skip(increase(), decrease()); + } + + /** + * Resets the indentation state to its initial empty stack and current indent. + */ + public void reset() { + stack.clear(); + current = ""; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + parser + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/tools/Production.java b/petitparser-core/src/main/java/org/petitparser/tools/Production.java new file mode 100644 index 0000000..cb7dcf2 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/tools/Production.java @@ -0,0 +1,59 @@ +package org.petitparser.tools; + +import java.util.Objects; + +/** + * A type-safe production key for {@link GrammarDefinition}. + * + * @param the parse result type produced by this production. + */ +public final class Production { + + private final String name; + + private Production(String name) { + this.name = Objects.requireNonNull(name, "Undefined name"); + } + + /** + * Creates a typed production key with the given {@code name}. + * + * @param name the production name. + * @param the parse result type. + * @return a production key. + */ + public static Production of(String name) { + return new Production<>(name); + } + + /** + * Returns the name of this production. + * + * @return the production name. + */ + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Production that = (Production) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "Production[" + name + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/Analyzer.java b/petitparser-core/src/main/java/org/petitparser/utils/Analyzer.java new file mode 100644 index 0000000..f1e8973 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/Analyzer.java @@ -0,0 +1,738 @@ +package org.petitparser.utils; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.actions.TrimmingParser; +import org.petitparser.parser.combinators.AndParser; +import org.petitparser.parser.combinators.ChoiceParser; +import org.petitparser.parser.combinators.EndOfInputParser; +import org.petitparser.parser.combinators.NotParser; +import org.petitparser.parser.combinators.OptionalParser; +import org.petitparser.parser.combinators.ResolvableParser; +import org.petitparser.parser.combinators.SequentialParser; +import org.petitparser.parser.primitive.EpsilonParser; +import org.petitparser.parser.primitive.FailureParser; +import org.petitparser.parser.primitive.PositionParser; +import org.petitparser.parser.primitive.StringParser; +import org.petitparser.parser.repeating.RepeatingCharacterParser; +import org.petitparser.parser.repeating.RepeatingParser; +import org.petitparser.parser.repeating.SeparatedRepeatingParser; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Reflective grammar graph analyzer. + */ +public class Analyzer { + + /** + * Constructs an {@link Analyzer} for the provided {@code root} parser. + * + * @param root the root parser of the grammar + * @return the analyzer instance + */ + public static Analyzer of(Parser root) { + return new Analyzer(root); + } + + private final Parser root; + private List reachableParsers; + private final Map> allChildrenCache = new HashMap<>(); + private Map nullableCache; + private Map> firstSetsCache; + private Map> followSetsCache; + + /** + * Constructs an {@link Analyzer} rooted at {@code root}. + * + * @param root the root parser + */ + public Analyzer(Parser root) { + this.root = Objects.requireNonNull(root, "Undefined root parser"); + } + + /** + * Returns the root parser of this analyzer. + * + * @return the root parser + */ + public Parser getRoot() { + return root; + } + + /** + * Returns all parsers reachable from the root in preorder DFS traversal. + * + * @return list of reachable parsers + */ + public List parsers() { + if (reachableParsers == null) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + Deque todo = new ArrayDeque<>(); + todo.push(root); + seen.add(root); + while (!todo.isEmpty()) { + Parser current = todo.pop(); + result.add(current); + List children = current.getChildren(); + for (int i = children.size() - 1; i >= 0; i--) { + Parser child = children.get(i); + if (seen.add(child)) { + todo.push(child); + } + } + } + reachableParsers = Collections.unmodifiableList(result); + } + return reachableParsers; + } + + /** + * Alias for {@link #parsers()}. + * + * @return list of reachable parsers + */ + public List getParsers() { + return parsers(); + } + + /** + * Returns a sequential {@link Stream} over all reachable parsers. + * + * @return stream of reachable parsers + */ + public Stream stream() { + return parsers().stream(); + } + + /** + * Returns all deep transitive children of {@code parser}. + * + * @param parser the parser + * @return unmodifiable set of all transitive descendants + */ + public Set allChildren(Parser parser) { + Objects.requireNonNull(parser, "Undefined parser"); + return allChildrenCache.computeIfAbsent(parser, p -> { + Set result = new LinkedHashSet<>(); + Deque todo = new ArrayDeque<>(p.getChildren()); + result.addAll(p.getChildren()); + while (!todo.isEmpty()) { + Parser current = todo.pop(); + for (Parser child : current.getChildren()) { + if (result.add(child)) { + todo.push(child); + } + } + } + return Collections.unmodifiableSet(result); + }); + } + + /** + * Finds the shortest path from the root parser to {@code target}. + * + * @param target the target parser + * @return path from root to target, or empty list if unreachable + */ + public List findPathTo(Parser target) { + return findPath(root, target); + } + + /** + * Finds the shortest path from the root parser to a parser matching {@code predicate}. + * + * @param predicate the condition to match + * @return path from root to matched parser, or empty list if unreachable + */ + public List findPathTo(Predicate predicate) { + return findPath(root, predicate); + } + + /** + * Finds the shortest path from {@code source} to {@code target}. + * + * @param source the source parser + * @param target the target parser + * @return path from source to target, or empty list if unreachable + */ + public List findPath(Parser source, Parser target) { + Objects.requireNonNull(target, "Undefined target parser"); + return findPath(source, target::equals); + } + + /** + * Finds the shortest path from {@code source} to a parser matching {@code predicate}. + * + * @param source the source parser + * @param predicate the condition to match + * @return path from source to matched parser, or empty list if unreachable + */ + public List findPath(Parser source, Predicate predicate) { + Objects.requireNonNull(source, "Undefined source parser"); + Objects.requireNonNull(predicate, "Undefined predicate"); + if (predicate.test(source)) { + return Collections.singletonList(source); + } + Queue queue = new ArrayDeque<>(); + Map parentMap = new HashMap<>(); + Set visited = new HashSet<>(); + queue.add(source); + visited.add(source); + Parser target = null; + while (!queue.isEmpty()) { + Parser current = queue.poll(); + for (Parser child : current.getChildren()) { + if (visited.add(child)) { + parentMap.put(child, current); + if (predicate.test(child)) { + target = child; + queue.clear(); + break; + } + queue.add(child); + } + } + } + if (target == null) { + return Collections.emptyList(); + } + List path = new ArrayList<>(); + for (Parser at = target; at != null; at = parentMap.get(at)) { + path.add(at); + } + Collections.reverse(path); + return Collections.unmodifiableList(path); + } + + /** + * Finds all simple paths from the root parser to {@code target}. + * + * @param target the target parser + * @return all simple paths from root to target + */ + public List> findAllPathsTo(Parser target) { + return findAllPaths(root, target); + } + + /** + * Finds all simple paths from the root parser to parsers matching {@code predicate}. + * + * @param predicate the condition to match + * @return all simple paths from root to matched parser + */ + public List> findAllPathsTo(Predicate predicate) { + return findAllPaths(root, predicate); + } + + /** + * Finds all simple paths from {@code source} to {@code target}. + * + * @param source the source parser + * @param target the target parser + * @return all simple paths from source to target + */ + public List> findAllPaths(Parser source, Parser target) { + Objects.requireNonNull(target, "Undefined target parser"); + return findAllPaths(source, target::equals); + } + + /** + * Finds all simple paths from {@code source} to parsers matching {@code predicate}. + * + * @param source the source parser + * @param predicate the condition to match + * @return all simple paths from source to matched parser + */ + public List> findAllPaths(Parser source, Predicate predicate) { + Objects.requireNonNull(source, "Undefined source parser"); + Objects.requireNonNull(predicate, "Undefined predicate"); + List> result = new ArrayList<>(); + List currentPath = new ArrayList<>(); + Set visited = new HashSet<>(); + findAllPathsHelper(source, predicate, visited, currentPath, result); + return Collections.unmodifiableList(result); + } + + private void findAllPathsHelper( + Parser current, + Predicate predicate, + Set visited, + List currentPath, + List> result) { + currentPath.add(current); + visited.add(current); + if (predicate.test(current)) { + result.add(List.copyOf(currentPath)); + } + for (Parser child : current.getChildren()) { + if (!visited.contains(child)) { + findAllPathsHelper(child, predicate, visited, currentPath, result); + } + } + visited.remove(current); + currentPath.removeLast(); + } + + /** + * Finds a cycle path from {@code parser} back to itself, if present. + * + * @param parser the parser to inspect + * @return cycle path starting and ending with parser, or empty list if no cycle + */ + public List findCycle(Parser parser) { + Objects.requireNonNull(parser, "Undefined parser"); + List shortestCycle = Collections.emptyList(); + for (Parser child : parser.getChildren()) { + List path = findPath(child, parser); + if (!path.isEmpty()) { + if (shortestCycle.isEmpty() || path.size() + 1 < shortestCycle.size()) { + List cycle = new ArrayList<>(); + cycle.add(parser); + cycle.addAll(path); + shortestCycle = Collections.unmodifiableList(cycle); + } + } + } + return shortestCycle; + } + + /** + * Returns whether {@code parser} is part of a cycle. + * + * @param parser the parser to check + * @return true if cyclic + */ + public boolean isCyclic(Parser parser) { + return allChildren(parser).contains(parser); + } + + /** + * Returns whether any cycle exists in the reachable grammar graph. + * + * @return true if any cyclic parser is present + */ + public boolean isCyclic() { + return parsers().stream().anyMatch(this::isCyclic); + } + + /** + * Returns all parsers that participate in cycles with {@code parser}. + * + * @param parser the parser to check + * @return set of parsers in cycle with parser, or empty set if parser is not cyclic + */ + public Set cycleSet(Parser parser) { + Objects.requireNonNull(parser, "Undefined parser"); + Set children = allChildren(parser); + if (!children.contains(parser)) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet<>(); + result.add(parser); + for (Parser child : children) { + if (child == parser || allChildren(child).contains(parser)) { + result.add(child); + } + } + return Collections.unmodifiableSet(result); + } + + /** + * Returns all parsers that participate in cycles in this grammar. + * + * @return set of all cyclic parsers + */ + public Set cycles() { + return parsers().stream() + .filter(this::isCyclic) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Returns whether {@code parser} is nullable (can succeed matching the empty string). + * + * @param parser the parser to check + * @return true if nullable + */ + public boolean isNullable(Parser parser) { + Objects.requireNonNull(parser, "Undefined parser"); + if (nullableCache == null) { + nullableCache = computeNullability(); + } + Boolean cached = nullableCache.get(parser); + if (cached != null) { + return cached; + } + return Analyzer.of(parser).isNullable(parser); + } + + private Map computeNullability() { + Map nullable = new HashMap<>(); + List all = parsers(); + for (Parser p : all) { + if (p instanceof EpsilonParser || p instanceof PositionParser || + p instanceof OptionalParser || p instanceof EndOfInputParser) { + nullable.put(p, true); + } else if (p instanceof StringParser && ((StringParser) p).getValue().isEmpty()) { + nullable.put(p, true); + } else if (p instanceof RepeatingCharacterParser && ((RepeatingCharacterParser) p).getMin() == 0) { + nullable.put(p, true); + } else if (p instanceof RepeatingParser && ((RepeatingParser) p).getMin() == 0) { + nullable.put(p, true); + } else { + nullable.put(p, false); + } + } + + boolean changed = true; + int iterations = 0; + int maxIterations = all.size() * 2 + 2; + while (changed && iterations++ < maxIterations) { + changed = false; + for (Parser p : all) { + boolean wasNullable = Boolean.TRUE.equals(nullable.get(p)); + boolean nowNullable = wasNullable; + if (p instanceof EpsilonParser || p instanceof PositionParser || + p instanceof OptionalParser || p instanceof EndOfInputParser) { + nowNullable = true; + } else if (p instanceof StringParser && ((StringParser) p).getValue().isEmpty()) { + nowNullable = true; + } else if (p instanceof RepeatingCharacterParser rcp) { + nowNullable = rcp.getMin() == 0; + } else if (p instanceof SeparatedRepeatingParser srp) { + if (srp.getMin() == 0) { + nowNullable = true; + } else if (srp.getMin() == 1) { + nowNullable = Boolean.TRUE.equals(nullable.get(srp.getChildren().getFirst())); + } else { + nowNullable = Boolean.TRUE.equals(nullable.get(srp.getChildren().getFirst())) && + Boolean.TRUE.equals(nullable.get(srp.getChildren().get(1))); + } + } else if (p instanceof RepeatingParser rp) { + if (rp.getMin() == 0) { + nowNullable = true; + } else { + nowNullable = Boolean.TRUE.equals(nullable.get(rp.getChildren().getFirst())); + } + } else if (p instanceof TrimmingParser) { + Parser del = p.getChildren().getFirst(); + nowNullable = Boolean.TRUE.equals(nullable.get(del)); + } else if (p instanceof SequentialParser) { + nowNullable = p.getChildren().stream() + .allMatch(c -> Boolean.TRUE.equals(nullable.get(c))); + } else if (p instanceof ChoiceParser) { + nowNullable = p.getChildren().stream() + .anyMatch(c -> Boolean.TRUE.equals(nullable.get(c))); + } else if (p instanceof NotParser) { + Parser child = p.getChildren().getFirst(); + nowNullable = !Boolean.TRUE.equals(nullable.get(child)); + } else if (p instanceof AndParser) { + Parser child = p.getChildren().getFirst(); + nowNullable = Boolean.TRUE.equals(nullable.get(child)); + } else if (!p.getChildren().isEmpty()) { + nowNullable = p.getChildren().stream() + .allMatch(c -> Boolean.TRUE.equals(nullable.get(c))); + } + if (wasNullable != nowNullable) { + nullable.put(p, nowNullable); + changed = true; + } + } + } + return nullable; + } + + private boolean isTerminal(Parser parser) { + return parser.getChildren().isEmpty() && + !(parser instanceof EpsilonParser) && + !(parser instanceof FailureParser) && + !(parser instanceof PositionParser); + } + + /** + * Returns the FIRST-set of {@code parser}: the terminal parsers that can appear first. + * + * @param parser the parser + * @return set of terminal parsers + */ + public Set firstSet(Parser parser) { + Objects.requireNonNull(parser, "Undefined parser"); + if (firstSetsCache == null) { + firstSetsCache = computeFirstSets(); + } + Set cached = firstSetsCache.get(parser); + if (cached != null) { + return cached; + } + return Analyzer.of(parser).firstSet(parser); + } + + private Map> computeFirstSets() { + Map> firstSets = new HashMap<>(); + List all = parsers(); + for (Parser p : all) { + Set set = new HashSet<>(); + if (isTerminal(p)) { + set.add(p); + } + firstSets.put(p, set); + } + + boolean changed = true; + int iterations = 0; + int maxIterations = all.size() * all.size() + 1; + while (changed && iterations++ < maxIterations) { + changed = false; + for (Parser p : all) { + if (isTerminal(p)) { + continue; + } + Set currentSet = firstSets.get(p); + int beforeSize = currentSet.size(); + + if (p instanceof TrimmingParser) { + Parser del = p.getChildren().getFirst(); + Parser left = p.getChildren().get(1); + Parser right = p.getChildren().get(2); + currentSet.addAll(firstSets.get(left)); + currentSet.addAll(firstSets.get(del)); + if (isNullable(del)) { + currentSet.addAll(firstSets.get(right)); + } + } else if (p instanceof SeparatedRepeatingParser) { + Parser del = p.getChildren().getFirst(); + Parser sep = p.getChildren().get(1); + currentSet.addAll(firstSets.get(del)); + if (isNullable(del)) { + currentSet.addAll(firstSets.get(sep)); + } + } else if (p instanceof SequentialParser) { + for (Parser child : p.getChildren()) { + currentSet.addAll(firstSets.get(child)); + if (!isNullable(child)) { + break; + } + } + } else if (p instanceof NotParser) { + // Lookahead negation consumes no terminals and never starts with its delegate. + } else { + for (Parser child : p.getChildren()) { + currentSet.addAll(firstSets.get(child)); + } + } + + if (currentSet.size() > beforeSize) { + changed = true; + } + } + } + + Map> unmodifiable = new HashMap<>(); + for (Map.Entry> entry : firstSets.entrySet()) { + unmodifiable.put(entry.getKey(), Collections.unmodifiableSet(entry.getValue())); + } + return unmodifiable; + } + + /** + * Returns the FOLLOW-set of {@code parser}: the terminal parsers that can immediately succeed it. + * + * @param parser the parser + * @return set of terminal parsers + */ + public Set followSet(Parser parser) { + Objects.requireNonNull(parser, "Undefined parser"); + if (followSetsCache == null) { + followSetsCache = computeFollowSets(); + } + return followSetsCache.getOrDefault(parser, Collections.emptySet()); + } + + private Map> computeFollowSets() { + Map> followSets = new HashMap<>(); + List all = parsers(); + for (Parser p : all) { + followSets.put(p, new HashSet<>()); + } + + boolean changed = true; + int iterations = 0; + int maxIterations = all.size() * all.size() + 1; + while (changed && iterations++ < maxIterations) { + changed = false; + for (Parser p : all) { + if (p instanceof TrimmingParser) { + Parser del = p.getChildren().getFirst(); + Parser left = p.getChildren().get(1); + Parser right = p.getChildren().get(2); + if (followSets.get(left).addAll(firstSet(left))) changed = true; + if (followSets.get(left).addAll(firstSet(del))) changed = true; + if (isNullable(del)) { + if (followSets.get(left).addAll(firstSet(right))) changed = true; + if (followSets.get(left).addAll(followSets.get(p))) changed = true; + } + if (followSets.get(del).addAll(firstSet(right))) changed = true; + if (followSets.get(del).addAll(followSets.get(p))) changed = true; + if (followSets.get(right).addAll(firstSet(right))) changed = true; + if (followSets.get(right).addAll(followSets.get(p))) changed = true; + } else if (p instanceof SeparatedRepeatingParser) { + Parser del = p.getChildren().getFirst(); + Parser sep = p.getChildren().get(1); + if (followSets.get(del).addAll(firstSet(sep))) changed = true; + if (followSets.get(del).addAll(followSets.get(p))) changed = true; + if (isNullable(sep)) { + if (followSets.get(del).addAll(firstSet(del))) changed = true; + } + if (followSets.get(sep).addAll(firstSet(del))) changed = true; + if (isNullable(del)) { + if (followSets.get(sep).addAll(firstSet(sep))) changed = true; + if (followSets.get(sep).addAll(followSets.get(p))) changed = true; + } + } else if (p instanceof RepeatingParser) { + Parser child = p.getChildren().getFirst(); + if (followSets.get(child).addAll(firstSet(child))) changed = true; + if (followSets.get(child).addAll(followSets.get(p))) changed = true; + } else if (p instanceof SequentialParser) { + List children = p.getChildren(); + for (int i = 0; i < children.size(); i++) { + Parser ci = children.get(i); + boolean allSubsequentNullable = true; + for (int j = i + 1; j < children.size(); j++) { + Parser cj = children.get(j); + if (followSets.get(ci).addAll(firstSet(cj))) { + changed = true; + } + if (!isNullable(cj)) { + allSubsequentNullable = false; + break; + } + } + if (allSubsequentNullable) { + if (followSets.get(ci).addAll(followSets.get(p))) { + changed = true; + } + } + } + } else { + for (Parser child : p.getChildren()) { + if (followSets.get(child).addAll(followSets.get(p))) { + changed = true; + } + } + } + } + } + + Map> unmodifiable = new HashMap<>(); + for (Map.Entry> entry : followSets.entrySet()) { + unmodifiable.put(entry.getKey(), Collections.unmodifiableSet(entry.getValue())); + } + return unmodifiable; + } + + /** + * Resolves all {@link ResolvableParser} references in {@code parser} into direct + * cycles/graphs without delegate overhead. + * + * @param parser the parser to resolve + * @return the resolved parser + */ + public static Parser resolve(Parser parser) { + Objects.requireNonNull(parser, "Undefined parser"); + Set seen = new HashSet<>(); + Deque todo = new ArrayDeque<>(); + todo.push(parser); + seen.add(parser); + while (!todo.isEmpty()) { + Parser current = todo.pop(); + for (Parser child : current.getChildren()) { + if (seen.add(child)) { + todo.push(child); + } + } + } + + Map mapping = new HashMap<>(); + for (Parser p : seen) { + if (p instanceof ResolvableParser) { + ResolvableParser resolvable = (ResolvableParser) p; + Set cycleDetection = new HashSet<>(); + cycleDetection.add(resolvable); + Parser target = resolvable.resolve(); + while (target instanceof ResolvableParser) { + if (!cycleDetection.add((ResolvableParser) target)) { + throw new IllegalStateException("Cyclic resolvable parser reference: " + target); + } + target = ((ResolvableParser) target).resolve(); + } + mapping.put(resolvable, target); + } + } + + for (Parser target : mapping.values()) { + if (seen.add(target)) { + todo.push(target); + while (!todo.isEmpty()) { + Parser current = todo.pop(); + for (Parser child : current.getChildren()) { + if (seen.add(child)) { + todo.push(child); + } + } + } + } + } + + for (Parser p : seen) { + for (Parser child : p.getChildren()) { + if (child instanceof ResolvableParser) { + @SuppressWarnings("unlikely-arg-type") + Parser replacement = mapping.get(child); + if (replacement != null) { + p.replace(child, replacement); + } + } + } + } + + if (parser instanceof ResolvableParser) { + return mapping.getOrDefault((ResolvableParser) parser, parser); + } + return parser; + } + + /** + * Resolves all {@link ResolvableParser} references in the root parser of this analyzer. + * + * @return the resolved root parser + */ + public Parser resolve() { + Parser resolved = resolve(root); + reachableParsers = null; + allChildrenCache.clear(); + nullableCache = null; + firstSetsCache = null; + followSetsCache = null; + return resolved; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " of " + root; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/FailureJoiner.java b/petitparser-core/src/main/java/org/petitparser/utils/FailureJoiner.java index 4c29e98..9c5176d 100644 --- a/petitparser-core/src/main/java/org/petitparser/utils/FailureJoiner.java +++ b/petitparser-core/src/main/java/org/petitparser/utils/FailureJoiner.java @@ -18,6 +18,16 @@ class SelectFirst implements FailureJoiner { public Failure apply(Failure first, Failure second) { return first; } + + @Override + public boolean equals(Object obj) { + return obj != null && getClass() == obj.getClass(); + } + + @Override + public int hashCode() { + return SelectFirst.class.hashCode(); + } } /** @@ -28,6 +38,16 @@ class SelectLast implements FailureJoiner { public Failure apply(Failure first, Failure second) { return second; } + + @Override + public boolean equals(Object obj) { + return obj != null && getClass() == obj.getClass(); + } + + @Override + public int hashCode() { + return SelectLast.class.hashCode(); + } } /** @@ -39,6 +59,16 @@ class SelectFarthest implements FailureJoiner { public Failure apply(Failure first, Failure second) { return first.getPosition() <= second.getPosition() ? second : first; } + + @Override + public boolean equals(Object obj) { + return obj != null && getClass() == obj.getClass(); + } + + @Override + public int hashCode() { + return SelectFarthest.class.hashCode(); + } } /** @@ -65,5 +95,22 @@ public Failure apply(Failure first, Failure second) { : first.failure(first.getMessage() + messageJoiner + second.getMessage()); } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SelectFarthestJoined other = (SelectFarthestJoined) obj; + return java.util.Objects.equals(messageJoiner, other.messageJoiner); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(SelectFarthestJoined.class, messageJoiner); + } } } diff --git a/petitparser-core/src/main/java/org/petitparser/utils/Linter.java b/petitparser-core/src/main/java/org/petitparser/utils/Linter.java new file mode 100644 index 0000000..af47bd7 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/Linter.java @@ -0,0 +1,230 @@ +package org.petitparser.utils; + +import org.petitparser.parser.Parser; +import org.petitparser.utils.linter.CharacterRepeaterRule; +import org.petitparser.utils.linter.DuplicateParserRule; +import org.petitparser.utils.linter.LeftRecursionRule; +import org.petitparser.utils.linter.LinterIssue; +import org.petitparser.utils.linter.LinterRule; +import org.petitparser.utils.linter.LinterType; +import org.petitparser.utils.linter.NestedChoiceRule; +import org.petitparser.utils.linter.NullableRepeaterRule; +import org.petitparser.utils.linter.OverlappingChoiceRule; +import org.petitparser.utils.linter.RepeatedChoiceRule; +import org.petitparser.utils.linter.UnnecessaryFlattenRule; +import org.petitparser.utils.linter.UnnecessaryResolvableRule; +import org.petitparser.utils.linter.UnoptimizedFlattenRule; +import org.petitparser.utils.linter.UnreachableChoiceRule; +import org.petitparser.utils.linter.UnresolvedSettableRule; +import org.petitparser.utils.linter.UnusedResultRule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Grammar linter engine executing lint rules over parser graphs. + */ +public class Linter { + + private Linter() {} + + /** + * All 13 default linter rules. + */ + public static final List ALL_RULES = List.of( + new CharacterRepeaterRule(), + new DuplicateParserRule(), + new LeftRecursionRule(), + new NestedChoiceRule(), + new NullableRepeaterRule(), + new OverlappingChoiceRule(), + new RepeatedChoiceRule(), + new UnnecessaryFlattenRule(), + new UnnecessaryResolvableRule(), + new UnoptimizedFlattenRule(), + new UnreachableChoiceRule(), + new UnresolvedSettableRule(), + new UnusedResultRule() + ); + + /** + * Default excluded types (rules of {@link LinterType#INFO} are ignored by default). + */ + public static final Set DEFAULT_EXCLUDED_TYPES = Collections.singleton(LinterType.INFO); + + /** + * Lints the grammar rooted at {@code parser} using default rules. + * + * @param parser the root parser + * @return list of identified issues + */ + public static List lint(Parser parser) { + return lint(parser, null, null, Collections.emptySet(), DEFAULT_EXCLUDED_TYPES); + } + + /** + * Lints the grammar rooted at {@code parser} using custom rules. + * + * @param parser the root parser + * @param rules custom rules to execute + * @return list of identified issues + */ + public static List lint(Parser parser, List rules) { + return lint(parser, null, rules, Collections.emptySet(), Collections.emptySet()); + } + + /** + * Lints the grammar rooted at {@code parser} using custom rules. + * + * @param parser the root parser + * @param rules custom rules to execute + * @return list of identified issues + */ + public static List lint(Parser parser, LinterRule... rules) { + return lint(parser, Arrays.asList(rules)); + } + + /** + * Lints the grammar rooted at {@code parser} with an issue callback. + * + * @param parser the root parser + * @param callback consumer called on each issue + * @return list of identified issues + */ + public static List lint(Parser parser, Consumer callback) { + return lint(parser, callback, null, Collections.emptySet(), DEFAULT_EXCLUDED_TYPES); + } + + /** + * Lints the grammar rooted at {@code parser} with an issue callback and custom rules. + * + * @param parser the root parser + * @param callback consumer called on each issue + * @param rules custom rules to execute + * @return list of identified issues + */ + public static List lint( + Parser parser, Consumer callback, List rules) { + return lint(parser, callback, rules, Collections.emptySet(), Collections.emptySet()); + } + + /** + * Fully configurable lint execution. + * + * @param parser the root parser + * @param callback optional consumer called on each issue + * @param rules custom rules, or null to use {@link #ALL_RULES} + * @param excludedRules set of rule titles to exclude when using default rules + * @param excludedTypes set of issue types to exclude when using default rules + * @return list of identified issues + */ + public static List lint( + Parser parser, + Consumer callback, + List rules, + Set excludedRules, + Set excludedTypes) { + Objects.requireNonNull(parser, "Undefined parser"); + List issues = new ArrayList<>(); + Analyzer analyzer = Analyzer.of(parser); + List selectedRules; + if (rules != null) { + selectedRules = rules; + } else { + Set excRules = excludedRules != null ? excludedRules : Collections.emptySet(); + Set excTypes = excludedTypes != null ? excludedTypes : DEFAULT_EXCLUDED_TYPES; + selectedRules = ALL_RULES.stream() + .filter(rule -> !excRules.contains(rule.getTitle()) && !excTypes.contains(rule.getType())) + .collect(Collectors.toList()); + } + + for (Parser current : analyzer.parsers()) { + for (LinterRule rule : selectedRules) { + rule.run(analyzer, current, issue -> { + if (callback != null) { + callback.accept(issue); + } + issues.add(issue); + }); + } + } + return Collections.unmodifiableList(issues); + } + + /** + * Creates a fluent query builder to configure and run the linter. + * + * @param parser the root parser + * @return a new query builder + */ + public static Query query(Parser parser) { + return new Query(parser); + } + + /** + * Fluent configuration query for linting. + */ + public static class Query { + private final Parser parser; + private Consumer callback; + private List rules; + private final Set excludedRules = new HashSet<>(); + private final Set excludedTypes = new HashSet<>(DEFAULT_EXCLUDED_TYPES); + + public Query(Parser parser) { + this.parser = Objects.requireNonNull(parser, "Undefined parser"); + } + + public Query onIssue(Consumer callback) { + this.callback = callback; + return this; + } + + public Query rules(List rules) { + this.rules = rules; + return this; + } + + public Query rules(LinterRule... rules) { + this.rules = Arrays.asList(rules); + return this; + } + + public Query excludeRule(String ruleTitle) { + this.excludedRules.add(ruleTitle); + return this; + } + + public Query excludeRules(Collection ruleTitles) { + this.excludedRules.addAll(ruleTitles); + return this; + } + + public Query excludeType(LinterType type) { + this.excludedTypes.add(type); + return this; + } + + public Query excludeTypes(Collection types) { + this.excludedTypes.addAll(types); + return this; + } + + public Query includeAllTypes() { + this.excludedTypes.clear(); + return this; + } + + public List lint() { + return Linter.lint(parser, callback, rules, excludedRules, excludedTypes); + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/Optimizer.java b/petitparser-core/src/main/java/org/petitparser/utils/Optimizer.java index d0920fc..15e3ae8 100644 --- a/petitparser-core/src/main/java/org/petitparser/utils/Optimizer.java +++ b/petitparser-core/src/main/java/org/petitparser/utils/Optimizer.java @@ -1,13 +1,17 @@ package org.petitparser.utils; import org.petitparser.parser.Parser; -import org.petitparser.parser.combinators.DelegateParser; -import org.petitparser.parser.combinators.SettableParser; +import org.petitparser.utils.optimizer.CharacterRepeaterRule; +import org.petitparser.utils.optimizer.FlattenChoiceRule; +import org.petitparser.utils.optimizer.OptimizeRule; +import org.petitparser.utils.optimizer.RemoveDelegateRule; +import org.petitparser.utils.optimizer.RemoveDuplicateRule; import java.util.ArrayList; -import java.util.HashSet; +import java.util.Arrays; +import java.util.Collections; import java.util.List; -import java.util.Optional; +import java.util.Objects; import java.util.Set; import java.util.function.Function; @@ -16,52 +20,140 @@ */ public class Optimizer { - private final List> transformers = new ArrayList<>(); + /** + * All standard optimizer rules in recommended application order. + */ + public static final List ALL_RULES = List.of( + new RemoveDelegateRule(), + new FlattenChoiceRule(), + new CharacterRepeaterRule(), + new RemoveDuplicateRule() + ); + + private final List rules = new ArrayList<>(); + + public Optimizer() {} + + public Optimizer(OptimizeRule... rules) { + this(Arrays.asList(rules)); + } + + public Optimizer(Iterable rules) { + if (rules != null) { + for (OptimizeRule rule : rules) { + this.rules.add(Objects.requireNonNull(rule, "Undefined rule")); + } + } + } /** - * Adds a generic transformer. + * Returns an unmodifiable list of the optimization rules configured on this optimizer. + */ + public List getRules() { + return Collections.unmodifiableList(rules); + } + + /** + * Adds an optimization rule. + */ + public Optimizer add(OptimizeRule rule) { + rules.add(Objects.requireNonNull(rule, "Undefined rule")); + return this; + } + + /** + * Adds a generic transformer function. */ public Optimizer add(Function transformer) { - transformers.add(transformer); + rules.add(OptimizeRule.of(transformer)); + return this; + } + + /** + * Adds multiple optimization rules. + */ + public Optimizer add(OptimizeRule... rules) { + return addAll(Arrays.asList(rules)); + } + + /** + * Adds all specified optimization rules. + */ + public Optimizer addAll(Iterable rules) { + for (OptimizeRule rule : rules) { + add(rule); + } return this; } /** - * Adds a transformer that removes unnecessary delegates. + * Adds a rule that removes unnecessary delegates (such as SettableParser or direct DelegateParser). */ public Optimizer removeDelegates() { - return add(parser -> { - while (DelegateParser.class.equals(parser.getClass()) || - SettableParser.class.equals(parser.getClass())) { - parser = parser.getChildren().get(0); - } - return parser; - }); + return add(new RemoveDelegateRule()); } /** - * Adds a transformer that collapses unnecessary copies of parsers. + * Adds a rule that collapses duplicate structurally-equal parser instances. */ public Optimizer removeDuplicates() { - Set uniques = new HashSet<>(); - return add(parser -> { - Optional target = uniques.stream() - .filter(each -> parser != each && parser.isEqualTo(each)).findFirst(); - if (target.isPresent()) { - return target.get(); - } else { - uniques.add(parser); - return parser; - } - }); + return add(new RemoveDuplicateRule()); + } + + /** + * Adds a rule that collapses duplicate structurally-equal parser instances with a specific set of seen parsers. + */ + public Optimizer removeDuplicates(Set uniques) { + return add(new RemoveDuplicateRule(uniques)); + } + + /** + * Adds a rule that flattens nested choices {@code [a, [b, c]]} into {@code [a, b, c]}. + */ + public Optimizer flattenChoices() { + return add(new FlattenChoiceRule()); + } + + /** + * Adds a rule that transforms {@code FlattenParser(PossessiveRepeatingParser(CharacterParser))} into {@code RepeatingCharacterParser}. + */ + public Optimizer characterRepeaters() { + return add(new CharacterRepeaterRule()); + } + + /** + * Alias for {@link #characterRepeaters()}. + */ + public Optimizer optimizeCharacterRepeaters() { + return characterRepeaters(); + } + + /** + * Configures all standard optimization rules (remove delegates, flatten choices, character repeaters, remove duplicates). + */ + public Optimizer all() { + return removeDelegates() + .flattenChoices() + .characterRepeaters() + .removeDuplicates(); + } + + /** + * Optimizes the provided parser using all standard optimization rules. + */ + public static Parser optimize(Parser parser) { + Objects.requireNonNull(parser, "Undefined parser"); + return new Optimizer().all().transform(parser); } /** * Transforms the provided parsers using the selected optimizations. */ public Parser transform(Parser parser) { + Objects.requireNonNull(parser, "Undefined parser"); + rules.forEach(OptimizeRule::reset); Function transformer = - transformers.stream().reduce(Function::andThen) + rules.stream().map(r -> (Function) r).reduce(Function::andThen) .orElse(Function.identity()); return Mirror.of(parser).transform(transformer); } diff --git a/petitparser-core/src/main/java/org/petitparser/utils/Progress.java b/petitparser-core/src/main/java/org/petitparser/utils/Progress.java new file mode 100644 index 0000000..090ba68 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/Progress.java @@ -0,0 +1,266 @@ +package org.petitparser.utils; + +import org.petitparser.context.Context; +import org.petitparser.parser.Parser; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Visual execution debugger that tracks parser activation and progress through the input. + */ +public final class Progress { + + private Progress() { + } + + /** + * Returns a parser that prints progress information to standard output during parsing. + */ + public static Parser of(Parser root) { + return progress(root); + } + + /** + * Returns a parser that calls the provided {@code observer} with a {@link ProgressFrame} + * whenever a parser is activated during parsing. + */ + public static Parser of(Parser root, Consumer observer) { + return progress(root, observer); + } + + /** + * Returns a parser that calls the provided {@code observer} with a {@link ProgressFrame} + * whenever a parser matching {@code predicate} is activated during parsing. + */ + public static Parser of( + Parser root, Predicate predicate, Consumer observer) { + return progress(root, predicate, observer); + } + + /** + * Returns a parser that prints progress information to standard output during parsing. + */ + public static Parser progress(Parser root) { + return progress(root, System.out::println); + } + + /** + * Returns a parser that calls the provided {@code observer} with a {@link ProgressFrame} + * whenever a parser is activated during parsing. + */ + public static Parser progress(Parser root, Consumer observer) { + return on(root, observer); + } + + /** + * Returns a parser that calls the provided {@code observer} with a {@link ProgressFrame} + * whenever a parser matching {@code predicate} is activated during parsing. + */ + public static Parser progress( + Parser root, Predicate predicate, Consumer observer) { + return on(root, predicate, observer); + } + + /** + * Returns a parser that calls the provided {@code observer} with a {@link ProgressFrame} + * whenever a parser is activated during parsing. + */ + public static Parser on(Parser root, Consumer observer) { + return on(root, parser -> true, observer); + } + + /** + * Returns a parser that calls the provided {@code observer} with a {@link ProgressFrame} + * whenever a parser matching {@code predicate} is activated during parsing. + */ + public static Parser on( + Parser root, Predicate predicate, Consumer observer) { + Objects.requireNonNull(root, "root parser must not be null"); + Objects.requireNonNull(predicate, "predicate must not be null"); + Objects.requireNonNull(observer, "observer must not be null"); + + ThreadLocal> stateHolder = ThreadLocal.withInitial(ArrayDeque::new); + Parser transformed = Mirror.of(root).transform(parser -> { + if (predicate.test(parser)) { + return parser.callCC((continuation, context) -> { + Deque stack = stateHolder.get(); + ProgressState current = stack.peek(); + if (current == null) { + current = new ProgressState(); + stack.push(current); + } + ProgressFrame frame = current.step(parser, context); + observer.accept(frame); + return continuation.apply(context); + }); + } else { + return parser; + } + }); + + return transformed.callCC((continuation, context) -> { + Deque stack = stateHolder.get(); + stack.push(new ProgressState()); + try { + return continuation.apply(context); + } finally { + stack.pop(); + if (stack.isEmpty()) { + stateHolder.remove(); + } + } + }); + } + + /** + * Internal state tracking progress across parser steps. + */ + private static class ProgressState { + private int previousPosition = -1; + private int maxPosition = -1; + + ProgressFrame step(Parser parser, Context context) { + int position = context.getPosition(); + boolean isBacktracking = previousPosition >= 0 && position < previousPosition; + int prev = previousPosition; + if (position > maxPosition) { + maxPosition = position; + } + int max = maxPosition; + previousPosition = position; + return new ProgressFrame(parser, context, position, prev, max, isBacktracking); + } + } + + /** + * Encapsulates the execution data of a progress step. + */ + public static class ProgressFrame { + + /** + * The parser being activated. + */ + public final Parser parser; + + /** + * The activation context. + */ + public final Context context; + + /** + * The current position in the input. + */ + public final int position; + + /** + * The position of the previous frame, or -1 if this is the first frame. + */ + public final int previousPosition; + + /** + * The highest position reached so far (at or before this frame). + */ + public final int maxPosition; + + /** + * Whether this frame jumped backwards relative to the immediately preceding frame. + */ + public final boolean isBacktracking; + + /** + * Constructs a progress frame with default backtracking values. + */ + public ProgressFrame(Parser parser, Context context) { + this(parser, context, context != null ? context.getPosition() : 0, -1, + context != null ? context.getPosition() : 0, false); + } + + /** + * Constructs a progress frame with explicit backtracking state. + */ + public ProgressFrame(Parser parser, Context context, boolean isBacktracking) { + this(parser, context, context != null ? context.getPosition() : 0, -1, + context != null ? context.getPosition() : 0, isBacktracking); + } + + /** + * Constructs a fully specified progress frame. + */ + public ProgressFrame( + Parser parser, Context context, int position, int previousPosition, + int maxPosition, boolean isBacktracking) { + this.parser = parser; + this.context = context; + this.position = position; + this.previousPosition = previousPosition; + this.maxPosition = maxPosition; + this.isBacktracking = isBacktracking; + } + + public Parser getParser() { + return parser; + } + + public Context getContext() { + return context; + } + + public int getPosition() { + return position; + } + + public int getPreviousPosition() { + return previousPosition; + } + + public int getMaxPosition() { + return maxPosition; + } + + public boolean isBacktracking() { + return isBacktracking; + } + + public boolean isBelowMaxPosition() { + return maxPosition >= 0 && position < maxPosition; + } + + public boolean isBacktracked() { + return isBelowMaxPosition(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + int count = Math.max(1, position + 1); + for (int i = 0; i < count; i++) { + builder.append('*'); + } + builder.append(' '); + builder.append(parser); + return builder.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProgressFrame that = (ProgressFrame) o; + return position == that.position && + previousPosition == that.previousPosition && + maxPosition == that.maxPosition && + isBacktracking == that.isBacktracking && + Objects.equals(parser, that.parser) && + Objects.equals(context, that.context); + } + + @Override + public int hashCode() { + return Objects.hash(parser, context, position, previousPosition, maxPosition, isBacktracking); + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/Tracer.java b/petitparser-core/src/main/java/org/petitparser/utils/Tracer.java index 340d8a9..3d56820 100644 --- a/petitparser-core/src/main/java/org/petitparser/utils/Tracer.java +++ b/petitparser-core/src/main/java/org/petitparser/utils/Tracer.java @@ -14,6 +14,8 @@ */ public class Tracer { + private Tracer() {} + /** * Returns a parser that calls the provided consumer with a {@link TraceEvent} * whenever a parser is activated or returning. diff --git a/petitparser-core/src/main/java/org/petitparser/utils/functions/Function3.java b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function3.java new file mode 100644 index 0000000..0b4c77f --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function3.java @@ -0,0 +1,29 @@ +package org.petitparser.utils.functions; + +import java.util.Objects; +import java.util.function.Function; + +/** + * A functional interface that accepts 3 arguments and produces a result. + * + * @param the type of the first argument + * @param the type of the second argument + * @param the type of the third argument + * @param the return type + */ +@FunctionalInterface +public interface Function3 { + + /** + * Applies this function to the given arguments. + */ + R apply(T1 t1, T2 t2, T3 t3); + + /** + * Returns a composed function that first applies this function, and then applies the {@code after} function. + */ + default Function3 andThen(Function after) { + Objects.requireNonNull(after, "Undefined after function"); + return (t1, t2, t3) -> after.apply(apply(t1, t2, t3)); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/functions/Function4.java b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function4.java new file mode 100644 index 0000000..aedf34a --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function4.java @@ -0,0 +1,30 @@ +package org.petitparser.utils.functions; + +import java.util.Objects; +import java.util.function.Function; + +/** + * A functional interface that accepts 4 arguments and produces a result. + * + * @param the type of the first argument + * @param the type of the second argument + * @param the type of the third argument + * @param the type of the fourth argument + * @param the return type + */ +@FunctionalInterface +public interface Function4 { + + /** + * Applies this function to the given arguments. + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4); + + /** + * Returns a composed function that first applies this function, and then applies the {@code after} function. + */ + default Function4 andThen(Function after) { + Objects.requireNonNull(after, "Undefined after function"); + return (t1, t2, t3, t4) -> after.apply(apply(t1, t2, t3, t4)); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/functions/Function5.java b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function5.java new file mode 100644 index 0000000..b277078 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function5.java @@ -0,0 +1,31 @@ +package org.petitparser.utils.functions; + +import java.util.Objects; +import java.util.function.Function; + +/** + * A functional interface that accepts 5 arguments and produces a result. + * + * @param the type of the first argument + * @param the type of the second argument + * @param the type of the third argument + * @param the type of the fourth argument + * @param the type of the fifth argument + * @param the return type + */ +@FunctionalInterface +public interface Function5 { + + /** + * Applies this function to the given arguments. + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5); + + /** + * Returns a composed function that first applies this function, and then applies the {@code after} function. + */ + default Function5 andThen(Function after) { + Objects.requireNonNull(after, "Undefined after function"); + return (t1, t2, t3, t4, t5) -> after.apply(apply(t1, t2, t3, t4, t5)); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/functions/Function6.java b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function6.java new file mode 100644 index 0000000..fb17fb9 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function6.java @@ -0,0 +1,32 @@ +package org.petitparser.utils.functions; + +import java.util.Objects; +import java.util.function.Function; + +/** + * A functional interface that accepts 6 arguments and produces a result. + * + * @param the type of the first argument + * @param the type of the second argument + * @param the type of the third argument + * @param the type of the fourth argument + * @param the type of the fifth argument + * @param the type of the sixth argument + * @param the return type + */ +@FunctionalInterface +public interface Function6 { + + /** + * Applies this function to the given arguments. + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6); + + /** + * Returns a composed function that first applies this function, and then applies the {@code after} function. + */ + default Function6 andThen(Function after) { + Objects.requireNonNull(after, "Undefined after function"); + return (t1, t2, t3, t4, t5, t6) -> after.apply(apply(t1, t2, t3, t4, t5, t6)); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/functions/Function7.java b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function7.java new file mode 100644 index 0000000..ed13894 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function7.java @@ -0,0 +1,33 @@ +package org.petitparser.utils.functions; + +import java.util.Objects; +import java.util.function.Function; + +/** + * A functional interface that accepts 7 arguments and produces a result. + * + * @param the type of the first argument + * @param the type of the second argument + * @param the type of the third argument + * @param the type of the fourth argument + * @param the type of the fifth argument + * @param the type of the sixth argument + * @param the type of the seventh argument + * @param the return type + */ +@FunctionalInterface +public interface Function7 { + + /** + * Applies this function to the given arguments. + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7); + + /** + * Returns a composed function that first applies this function, and then applies the {@code after} function. + */ + default Function7 andThen(Function after) { + Objects.requireNonNull(after, "Undefined after function"); + return (t1, t2, t3, t4, t5, t6, t7) -> after.apply(apply(t1, t2, t3, t4, t5, t6, t7)); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/functions/Function8.java b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function8.java new file mode 100644 index 0000000..03aa4ba --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function8.java @@ -0,0 +1,34 @@ +package org.petitparser.utils.functions; + +import java.util.Objects; +import java.util.function.Function; + +/** + * A functional interface that accepts 8 arguments and produces a result. + * + * @param the type of the first argument + * @param the type of the second argument + * @param the type of the third argument + * @param the type of the fourth argument + * @param the type of the fifth argument + * @param the type of the sixth argument + * @param the type of the seventh argument + * @param the type of the eighth argument + * @param the return type + */ +@FunctionalInterface +public interface Function8 { + + /** + * Applies this function to the given arguments. + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8); + + /** + * Returns a composed function that first applies this function, and then applies the {@code after} function. + */ + default Function8 andThen(Function after) { + Objects.requireNonNull(after, "Undefined after function"); + return (t1, t2, t3, t4, t5, t6, t7, t8) -> after.apply(apply(t1, t2, t3, t4, t5, t6, t7, t8)); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/functions/Function9.java b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function9.java new file mode 100644 index 0000000..dc51f69 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/functions/Function9.java @@ -0,0 +1,36 @@ +package org.petitparser.utils.functions; + +import java.util.Objects; +import java.util.function.Function; + +/** + * A functional interface that accepts 9 arguments and produces a result. + * + * @param the type of the first argument + * @param the type of the second argument + * @param the type of the third argument + * @param the type of the fourth argument + * @param the type of the fifth argument + * @param the type of the sixth argument + * @param the type of the seventh argument + * @param the type of the eighth argument + * @param the type of the ninth argument + * @param the return type + */ +@FunctionalInterface +public interface Function9 { + + /** + * Applies this function to the given arguments. + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9); + + /** + * Returns a composed function that first applies this function, and then applies the {@code after} function. + */ + default Function9 andThen( + Function after) { + Objects.requireNonNull(after, "Undefined after function"); + return (t1, t2, t3, t4, t5, t6, t7, t8, t9) -> after.apply(apply(t1, t2, t3, t4, t5, t6, t7, t8, t9)); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/CharacterRepeaterRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/CharacterRepeaterRule.java new file mode 100644 index 0000000..c20884e --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/CharacterRepeaterRule.java @@ -0,0 +1,40 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.actions.FlattenParser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.repeating.PossessiveRepeatingParser; +import org.petitparser.utils.Analyzer; + +import java.util.function.Consumer; + +/** + * Identifies {@code .star().flatten()} on character parsers and suggests {@code starString()}. + */ +public class CharacterRepeaterRule extends LinterRule { + + public CharacterRepeaterRule() { + super(LinterType.WARNING, "Character repeater"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof FlattenParser) { + Parser repeating = parser.getChildren().getFirst(); + if (repeating instanceof PossessiveRepeatingParser) { + Parser character = repeating.getChildren().getFirst(); + if (character instanceof CharacterParser) { + callback.accept(new LinterIssue( + this, + parser, + "A flattened repeater (" + repeating + ") that delegates to a character " + + "parser (" + character + ") can be much more efficiently implemented " + + "using `starString`, `plusString`, `timesString`, or " + + "`repeatString` that directly returns the underlying String " + + "instead of an intermediate List." + )); + } + } + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/DuplicateParserRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/DuplicateParserRule.java new file mode 100644 index 0000000..7794df2 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/DuplicateParserRule.java @@ -0,0 +1,33 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.utils.Analyzer; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Identifies duplicate structurally equal parser instances in grammar graph. + */ +public class DuplicateParserRule extends LinterRule { + + public DuplicateParserRule() { + super(LinterType.INFO, "Duplicate parser"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + List duplicates = analyzer.parsers().stream() + .filter(parser::isEqualTo) + .collect(Collectors.toList()); + if (duplicates.size() > 1 && duplicates.get(0) == parser) { + callback.accept(new LinterIssue( + this, + parser, + duplicates.size() + " instances of the same parser exist in this grammar. " + + "If possible, reuse the same parser instances to reduce memory footprint and increase performance." + )); + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/LeftRecursionRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/LeftRecursionRule.java new file mode 100644 index 0000000..b0961b8 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/LeftRecursionRule.java @@ -0,0 +1,31 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.utils.Analyzer; + +import java.util.Set; +import java.util.function.Consumer; + +/** + * Identifies left-recursive loops that lead to infinite recursion. + */ +public class LeftRecursionRule extends LinterRule { + + public LeftRecursionRule() { + super(LinterType.ERROR, "Left recursion"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + Set cycleSet = analyzer.cycleSet(parser); + if (!cycleSet.isEmpty()) { + callback.accept(new LinterIssue( + this, + parser, + "The parsers directly or indirectly refers to itself without consuming input:\n" + + formatIterable(cycleSet, 1) + "\n" + + "This causes an infinite loop when parsing." + )); + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/LinterIssue.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/LinterIssue.java new file mode 100644 index 0000000..9485dcf --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/LinterIssue.java @@ -0,0 +1,98 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; + +import java.util.Objects; + +/** + * Encapsulates a single issue identified by a {@link LinterRule}. + */ +public class LinterIssue { + + private final LinterRule rule; + private final Parser parser; + private final String description; + + /** + * Constructs a new linter issue. + * + * @param rule the rule that identified this issue + * @param parser the parser object with the issue + * @param description detailed explanation of the issue + */ + public LinterIssue(LinterRule rule, Parser parser, String description) { + this.rule = Objects.requireNonNull(rule, "Undefined rule"); + this.parser = Objects.requireNonNull(parser, "Undefined parser"); + this.description = Objects.requireNonNull(description, "Undefined description"); + } + + /** + * Returns the rule that identified the issue. + * + * @return the rule + */ + public LinterRule getRule() { + return rule; + } + + /** + * Returns the parser object with the issue. + * + * @return the parser + */ + public Parser getParser() { + return parser; + } + + /** + * Returns the detailed description of the issue. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Returns the severity type of the issue. + * + * @return the severity type + */ + public LinterType getType() { + return rule.getType(); + } + + /** + * Returns the title of the rule that reported this issue. + * + * @return the title + */ + public String getTitle() { + return rule.getTitle(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof LinterIssue)) { + return false; + } + LinterIssue other = (LinterIssue) obj; + return Objects.equals(rule, other.rule) && + Objects.equals(parser, other.parser) && + Objects.equals(description, other.description); + } + + @Override + public int hashCode() { + return Objects.hash(rule, parser, description); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(type: " + getType() + ", title: " + getTitle() + + ", parser: " + parser + ", description: " + description + ")"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/LinterRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/LinterRule.java new file mode 100644 index 0000000..8326fa1 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/LinterRule.java @@ -0,0 +1,175 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.utils.Analyzer; + +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Encapsulates a single grammar linter rule. + */ +public abstract class LinterRule { + + /** + * Functional interface for implementing a custom linter rule runner. + */ + @FunctionalInterface + public interface LinterFunction { + void run(LinterRule rule, Analyzer analyzer, Parser parser, Consumer callback); + } + + /** + * Constructs a custom {@link LinterRule} backed by a lambda function. + * + * @param type the severity type + * @param title the human-readable title + * @param function the function to execute on each parser + * @return the created rule + */ + public static LinterRule of(LinterType type, String title, LinterFunction function) { + Objects.requireNonNull(function, "Undefined function"); + return new LinterRule(type, title) { + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + function.run(this, analyzer, parser, callback); + } + }; + } + + private final LinterType type; + private final String title; + + /** + * Constructs a new linter rule. + * + * @param type the severity type + * @param title the human-readable title + */ + protected LinterRule(LinterType type, String title) { + this.type = Objects.requireNonNull(type, "Undefined type"); + this.title = Objects.requireNonNull(title, "Undefined title"); + } + + /** + * Returns the severity type of issues detected by this rule. + * + * @return the severity type + */ + public LinterType getType() { + return type; + } + + /** + * Returns the human-readable title of this rule. + * + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * Executes this rule using the provided {@code analyzer} on {@code parser}. + * + * @param analyzer the analyzer for grammar graph introspection + * @param parser the current parser node being inspected + * @param callback consumer called whenever an issue is detected + */ + public abstract void run(Analyzer analyzer, Parser parser, Consumer callback); + + /** + * Formats an iterable into a bulleted or numbered list. + * + * @param objects the items to format + * @param offset optional starting index for numbered list, or null for bulleted + * @return the formatted string + */ + public static String formatIterable(Iterable objects, Integer offset) { + Objects.requireNonNull(objects, "Undefined objects"); + StringBuilder buffer = new StringBuilder(); + int i = 0; + for (Object object : objects) { + if (i > 0) { + buffer.append('\n'); + } + if (offset != null) { + buffer.append(' ').append(offset + i).append(": "); + } else { + buffer.append(" - "); + } + buffer.append(object); + i++; + } + return buffer.toString(); + } + + /** + * Formats an iterable into a bulleted list. + * + * @param objects the items to format + * @return the formatted string + */ + public static String formatIterable(Iterable objects) { + return formatIterable(objects, null); + } + + /** + * Tests whether two iterables of parsers are structurally equal under {@link Parser#isEqualTo(Parser)}. + * + * @param first the first collection + * @param second the second collection + * @return true if all elements match pairwise structurally + */ + public static boolean isParserIterableEqual(Iterable first, Iterable second) { + Objects.requireNonNull(first, "Undefined first collection"); + Objects.requireNonNull(second, "Undefined second collection"); + for (Parser one : first) { + boolean found = false; + for (Parser two : second) { + if (one.isEqualTo(two)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + for (Parser two : second) { + boolean found = false; + for (Parser one : first) { + if (two.isEqualTo(one)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof LinterRule)) { + return false; + } + LinterRule other = (LinterRule) obj; + return type == other.type && Objects.equals(title, other.title); + } + + @Override + public int hashCode() { + return Objects.hash(type, title); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(type: " + type + ", title: " + title + ")"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/LinterType.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/LinterType.java new file mode 100644 index 0000000..53ff6d0 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/LinterType.java @@ -0,0 +1,10 @@ +package org.petitparser.utils.linter; + +/** + * The severity type of a linter issue. + */ +public enum LinterType { + INFO, + WARNING, + ERROR +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/NestedChoiceRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/NestedChoiceRule.java new file mode 100644 index 0000000..a1580d7 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/NestedChoiceRule.java @@ -0,0 +1,36 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ChoiceParser; +import org.petitparser.utils.Analyzer; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Identifies nested choice combinators. + */ +public class NestedChoiceRule extends LinterRule { + + public NestedChoiceRule() { + super(LinterType.INFO, "Nested choice"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof ChoiceParser) { + List children = parser.getChildren(); + for (int i = 0; i < children.size(); i++) { + Parser child = children.get(i); + if (child instanceof ChoiceParser) { + callback.accept(new LinterIssue( + this, + parser, + "The choice at index " + i + " is another choice (" + child + ") that adds " + + "unnecessary overhead that can be avoided by flattening it into the parent." + )); + } + } + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/NullableRepeaterRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/NullableRepeaterRule.java new file mode 100644 index 0000000..8247db3 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/NullableRepeaterRule.java @@ -0,0 +1,38 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.repeating.RepeatingParser; +import org.petitparser.parser.repeating.SeparatedRepeatingParser; +import org.petitparser.utils.Analyzer; + +import java.util.function.Consumer; + +/** + * Identifies repeating parsers over nullable delegates that lead to infinite loops. + */ +public class NullableRepeaterRule extends LinterRule { + + public NullableRepeaterRule() { + super(LinterType.ERROR, "Nullable repeater"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof RepeatingParser) { + Parser delegate = parser.getChildren().getFirst(); + if (analyzer.isNullable(delegate)) { + if (parser instanceof SeparatedRepeatingParser) { + Parser separator = parser.getChildren().get(1); + if (!analyzer.isNullable(separator)) { + return; + } + } + callback.accept(new LinterIssue( + this, + parser, + "A repeater that delegates to a nullable parser causes an infinite loop when parsing." + )); + } + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/OverlappingChoiceRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/OverlappingChoiceRule.java new file mode 100644 index 0000000..06c741e --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/OverlappingChoiceRule.java @@ -0,0 +1,42 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ChoiceParser; +import org.petitparser.utils.Analyzer; + +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * Identifies choices where a prefix choice shadows a later choice. + */ +public class OverlappingChoiceRule extends LinterRule { + + public OverlappingChoiceRule() { + super(LinterType.INFO, "Overlapping choice"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof ChoiceParser) { + List children = parser.getChildren(); + for (int i = 0; i < children.size(); i++) { + Set firstI = analyzer.firstSet(children.get(i)); + for (int j = i + 1; j < children.size(); j++) { + Set firstJ = analyzer.firstSet(children.get(j)); + if (!firstI.isEmpty() && isParserIterableEqual(firstI, firstJ)) { + callback.accept(new LinterIssue( + this, + parser, + "The choices at index " + i + " and " + j + " have overlapping first-sets, " + + "which can be an indication of an inefficient grammar:\n" + + formatIterable(firstI) + "\n" + + "If possible, try extracting common prefixes from choices." + )); + } + } + } + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/RepeatedChoiceRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/RepeatedChoiceRule.java new file mode 100644 index 0000000..f2b2b70 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/RepeatedChoiceRule.java @@ -0,0 +1,39 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ChoiceParser; +import org.petitparser.utils.Analyzer; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Identifies duplicate branches in a choice parser. + */ +public class RepeatedChoiceRule extends LinterRule { + + public RepeatedChoiceRule() { + super(LinterType.WARNING, "Repeated choice"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof ChoiceParser) { + List children = parser.getChildren(); + for (int i = 0; i < children.size(); i++) { + for (int j = i + 1; j < children.size(); j++) { + if (children.get(i).isEqualTo(children.get(j))) { + callback.accept(new LinterIssue( + this, + parser, + "The choices at index " + i + " and " + j + " are identical:\n" + + " " + i + ": " + children.get(i) + "\n" + + " " + j + ": " + children.get(j) + "\n" + + "The second choice can never succeed and can therefore be removed." + )); + } + } + } + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/UnnecessaryFlattenRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnnecessaryFlattenRule.java new file mode 100644 index 0000000..9663b5a --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnnecessaryFlattenRule.java @@ -0,0 +1,40 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.actions.FlattenParser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.NewlineParser; +import org.petitparser.parser.primitive.StringParser; +import org.petitparser.parser.repeating.RepeatingCharacterParser; +import org.petitparser.utils.Analyzer; + +import java.util.function.Consumer; + +/** + * Identifies {@code flatten()} on parsers already producing Strings. + */ +public class UnnecessaryFlattenRule extends LinterRule { + + public UnnecessaryFlattenRule() { + super(LinterType.WARNING, "Unnecessary flatten"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof FlattenParser && ((FlattenParser) parser).getMessage() == null) { + Parser delegate = parser.getChildren().get(0); + if (delegate instanceof CharacterParser || + delegate instanceof FlattenParser || + delegate instanceof NewlineParser || + delegate instanceof StringParser || + delegate instanceof RepeatingCharacterParser) { + callback.accept(new LinterIssue( + this, + parser, + "A flatten parser delegating to a parser (" + delegate + ") that is " + + "returning the accepted input string adds unnecessary overhead and can be removed." + )); + } + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/UnnecessaryResolvableRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnnecessaryResolvableRule.java new file mode 100644 index 0000000..bb84835 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnnecessaryResolvableRule.java @@ -0,0 +1,30 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ResolvableParser; +import org.petitparser.utils.Analyzer; + +import java.util.function.Consumer; + +/** + * Identifies unresolved resolvable wrappers. + */ +public class UnnecessaryResolvableRule extends LinterRule { + + public UnnecessaryResolvableRule() { + super(LinterType.WARNING, "Unnecessary resolvable"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof ResolvableParser) { + callback.accept(new LinterIssue( + this, + parser, + "Resolvable parsers are used during construction of recursive grammars. " + + "While they typically dispatch to their delegate, they add unnecessary " + + "overhead and can be avoided by removing them before parsing using `resolve(parser)`." + )); + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/UnoptimizedFlattenRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnoptimizedFlattenRule.java new file mode 100644 index 0000000..79d80ce --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnoptimizedFlattenRule.java @@ -0,0 +1,31 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.actions.FlattenParser; +import org.petitparser.utils.Analyzer; + +import java.util.function.Consumer; + +/** + * Identifies {@code flatten()} parsers without an error message preventing switch to fast parse mode. + */ +public class UnoptimizedFlattenRule extends LinterRule { + + public UnoptimizedFlattenRule() { + super(LinterType.INFO, "Unoptimized flatten"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof FlattenParser && ((FlattenParser) parser).getMessage() == null) { + callback.accept(new LinterIssue( + this, + parser, + "A flatten parser without an error message is unable to switch " + + "to the fast parsing mode. This can lead to inefficient parsers " + + "and can usually easily fixed by providing an error message " + + "that should be used in case the delegate fails to parse." + )); + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/UnreachableChoiceRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnreachableChoiceRule.java new file mode 100644 index 0000000..40133d6 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnreachableChoiceRule.java @@ -0,0 +1,37 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ChoiceParser; +import org.petitparser.utils.Analyzer; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Identifies choices located after unconditional matchers. + */ +public class UnreachableChoiceRule extends LinterRule { + + public UnreachableChoiceRule() { + super(LinterType.WARNING, "Unreachable choice"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof ChoiceParser) { + List children = parser.getChildren(); + for (int i = 0; i < children.size() - 1; i++) { + if (analyzer.isNullable(children.get(i))) { + callback.accept(new LinterIssue( + this, + parser, + "The choice at index " + i + " is nullable:\n" + + " " + i + ": " + children.get(i) + "\n" + + "thus the choices after that can never be reached and can be removed:\n" + + formatIterable(children.subList(i + 1, children.size()), i + 1) + )); + } + } + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/UnresolvedSettableRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnresolvedSettableRule.java new file mode 100644 index 0000000..11397f0 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnresolvedSettableRule.java @@ -0,0 +1,32 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.SettableParser; +import org.petitparser.parser.primitive.FailureParser; +import org.petitparser.utils.Analyzer; + +import java.util.function.Consumer; + +/** + * Identifies {@link SettableParser} left in undefined state. + */ +public class UnresolvedSettableRule extends LinterRule { + + public UnresolvedSettableRule() { + super(LinterType.ERROR, "Unresolved settable"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof SettableParser) { + Parser delegate = parser.getChildren().getFirst(); + if (delegate instanceof FailureParser) { + callback.accept(new LinterIssue( + this, + parser, + "This error is typically a bug in the code where a recursive grammar was created with `undefined()` that has not been resolved." + )); + } + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/linter/UnusedResultRule.java b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnusedResultRule.java new file mode 100644 index 0000000..848d6a7 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/linter/UnusedResultRule.java @@ -0,0 +1,66 @@ +package org.petitparser.utils.linter; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.actions.ActionParser; +import org.petitparser.parser.actions.CastListParser; +import org.petitparser.parser.actions.CastParser; +import org.petitparser.parser.actions.FlattenParser; +import org.petitparser.parser.actions.PermuteParser; +import org.petitparser.parser.actions.PickParser; +import org.petitparser.parser.actions.TokenParser; +import org.petitparser.parser.actions.WhereParser; +import org.petitparser.utils.Analyzer; + +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Identifies complex sub-parses whose results are discarded. + */ +public class UnusedResultRule extends LinterRule { + + public UnusedResultRule() { + super(LinterType.INFO, "Unused result"); + } + + @Override + public void run(Analyzer analyzer, Parser parser, Consumer callback) { + if (parser instanceof FlattenParser) { + Set deepChildren = analyzer.allChildren(parser); + Set ignoredResults = deepChildren.stream() + .filter(this::isResultProducing) + .collect(Collectors.toSet()); + if (!ignoredResults.isEmpty()) { + List path = analyzer.findPath(parser, ignoredResults::contains); + callback.accept(new LinterIssue( + this, + parser, + "The flatten parser discards the result of its children and " + + "instead returns the consumed input. Yet this flatten parser " + + "(indirectly) refers to one or more other parsers that explicitly " + + "produce a result which is then ignored when called from this context:\n" + + formatIterable(path, 1) + "\n" + + "This might point to an inefficient grammar or a possible bug." + )); + } + } + } + + protected boolean isResultProducing(Parser parser) { + if (parser instanceof CastParser || + parser instanceof CastListParser || + parser instanceof FlattenParser || + parser instanceof PermuteParser || + parser instanceof PickParser || + parser instanceof TokenParser || + parser instanceof WhereParser) { + return true; + } + if (parser instanceof ActionParser) { + return !((ActionParser) parser).hasSideEffects(); + } + return false; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/optimizer/CharacterRepeaterRule.java b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/CharacterRepeaterRule.java new file mode 100644 index 0000000..69be6fe --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/CharacterRepeaterRule.java @@ -0,0 +1,61 @@ +package org.petitparser.utils.optimizer; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.actions.FlattenParser; +import org.petitparser.parser.combinators.DelegateParser; +import org.petitparser.parser.combinators.SettableParser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.repeating.PossessiveRepeatingParser; +import org.petitparser.parser.repeating.RepeatingCharacterParser; + +import java.util.HashSet; +import java.util.Set; + +/** + * Transforms {@code FlattenParser(PossessiveRepeatingParser(CharacterParser))} into an optimized + * {@link RepeatingCharacterParser}. + */ +public class CharacterRepeaterRule implements OptimizeRule { + + @Override + public Parser apply(Parser parser) { + if (parser != null && FlattenParser.class.equals(parser.getClass())) { + FlattenParser flatten = (FlattenParser) parser; + if (!flatten.getChildren().isEmpty()) { + Parser delegate = unwrap(flatten.getChildren().getFirst()); + if (delegate != null && PossessiveRepeatingParser.class.equals(delegate.getClass())) { + PossessiveRepeatingParser repeating = (PossessiveRepeatingParser) delegate; + if (!repeating.getChildren().isEmpty()) { + Parser repeatingDelegate = unwrap(repeating.getChildren().getFirst()); + if (repeatingDelegate instanceof CharacterParser character) { + return character.repeatString( + repeating.getMin(), repeating.getMax(), flatten.getMessage()); + } + } + } + } + } + return parser; + } + + private Parser unwrap(Parser parser) { + Set seen = null; + while (parser != null && + (DelegateParser.class.equals(parser.getClass()) || + SettableParser.class.equals(parser.getClass())) && + !parser.getChildren().isEmpty()) { + if (seen == null) { + seen = new HashSet<>(); + } + if (!seen.add(parser)) { + break; + } + Parser next = parser.getChildren().getFirst(); + if (next == null) { + break; + } + parser = next; + } + return parser; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/optimizer/FlattenChoiceRule.java b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/FlattenChoiceRule.java new file mode 100644 index 0000000..543cb07 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/FlattenChoiceRule.java @@ -0,0 +1,58 @@ +package org.petitparser.utils.optimizer; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ChoiceParser; + +import org.petitparser.utils.FailureJoiner; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Flattens nested choices {@code [a, [b, c]]} into a single choice {@code [a, b, c]}. + */ +public class FlattenChoiceRule implements OptimizeRule { + + @Override + public Parser apply(Parser parser) { + if (parser != null && ChoiceParser.class.equals(parser.getClass())) { + ChoiceParser choice = (ChoiceParser) parser; + FailureJoiner joiner = choice.getFailureJoiner(); + boolean hasNested = false; + for (Parser child : choice.getChildren()) { + if (ChoiceParser.class.equals(child.getClass()) && + Objects.equals(joiner, ((ChoiceParser) child).getFailureJoiner())) { + hasNested = true; + break; + } + } + if (hasNested) { + List children = new ArrayList<>(); + flatten(choice, joiner, children, new HashSet<>()); + if (children.isEmpty() || children.equals(choice.getChildren())) { + return choice; + } + return new ChoiceParser(joiner, children.toArray(new Parser[0])); + } + } + return parser; + } + + private void flatten( + ChoiceParser choice, FailureJoiner expectedJoiner, List result, Set seen) { + if (!seen.add(choice)) { + return; + } + for (Parser child : choice.getChildren()) { + if (ChoiceParser.class.equals(child.getClass()) && + Objects.equals(expectedJoiner, ((ChoiceParser) child).getFailureJoiner())) { + flatten((ChoiceParser) child, expectedJoiner, result, seen); + } else { + result.add(child); + } + } + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/optimizer/OptimizeRule.java b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/OptimizeRule.java new file mode 100644 index 0000000..d5e8254 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/OptimizeRule.java @@ -0,0 +1,59 @@ +package org.petitparser.utils.optimizer; + +import org.petitparser.parser.Parser; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Encapsulates an optimization or transformation rule applied to a parser node. + */ +@FunctionalInterface +public interface OptimizeRule extends Function { + + /** + * Transforms the given {@code parser} into an optimized equivalent parser, + * or returns the parser itself if no optimization is applicable. + * + * @param parser the parser node to inspect and potentially optimize + * @return the optimized parser or original parser + */ + @Override + Parser apply(Parser parser); + + /** + * Transforms the given {@code parser} into an optimized equivalent parser. + * Defaults to delegating to {@link #apply(Parser)}. + * + * @param parser the parser node to inspect and potentially optimize + * @return the optimized parser or original parser + */ + default Parser optimize(Parser parser) { + return apply(parser); + } + + /** + * Returns the human-readable name of this optimization rule. + * + * @return the rule name + */ + default String getName() { + return getClass().getSimpleName(); + } + + /** + * Resets any state accumulated by this rule across transformation runs. + */ + default void reset() {} + + /** + * Adapts a {@link Function} to an {@link OptimizeRule}. + * + * @param function the function to wrap + * @return the optimize rule + */ + static OptimizeRule of(Function function) { + Objects.requireNonNull(function, "Undefined function"); + return function instanceof OptimizeRule ? (OptimizeRule) function : function::apply; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/optimizer/RemoveDelegateRule.java b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/RemoveDelegateRule.java new file mode 100644 index 0000000..f9582ee --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/RemoveDelegateRule.java @@ -0,0 +1,39 @@ +package org.petitparser.utils.optimizer; + +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.DelegateParser; +import org.petitparser.parser.combinators.SettableParser; + +import java.util.HashSet; +import java.util.Set; + +/** + * Removes unnecessary delegate parsers (such as direct {@link DelegateParser} or {@link SettableParser}) + * that only forward to an inner parser without adding behavior. + */ +public class RemoveDelegateRule implements OptimizeRule { + + @Override + public Parser apply(Parser parser) { + if (parser == null) { + return null; + } + Set seen = null; + while ((DelegateParser.class.equals(parser.getClass()) || + SettableParser.class.equals(parser.getClass())) && + !parser.getChildren().isEmpty()) { + if (seen == null) { + seen = new HashSet<>(); + } + if (!seen.add(parser)) { + break; + } + Parser next = parser.getChildren().get(0); + if (next == null) { + break; + } + parser = next; + } + return parser; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/optimizer/RemoveDuplicateRule.java b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/RemoveDuplicateRule.java new file mode 100644 index 0000000..4669099 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/optimizer/RemoveDuplicateRule.java @@ -0,0 +1,58 @@ +package org.petitparser.utils.optimizer; + +import org.petitparser.parser.Parser; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Collapses structurally equal duplicate parser instances in the grammar graph to point to a single instance. + */ +public class RemoveDuplicateRule implements OptimizeRule { + + private final Set uniques; + private final boolean clearOnReset; + + public RemoveDuplicateRule() { + this(new HashSet<>(), true); + } + + public RemoveDuplicateRule(Set uniques) { + this(uniques, false); + } + + private RemoveDuplicateRule(Set uniques, boolean clearOnReset) { + this.uniques = Objects.requireNonNull(uniques, "Undefined uniques"); + this.clearOnReset = clearOnReset; + } + + public Set getUniques() { + return uniques; + } + + public void clear() { + uniques.clear(); + } + + @Override + public void reset() { + if (clearOnReset) { + clear(); + } + } + + @Override + public Parser apply(Parser parser) { + if (parser == null) { + return null; + } + for (Parser each : uniques) { + if (parser != each && parser.isEqualTo(each)) { + return each; + } + } + uniques.add(parser); + return parser; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple.java b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple.java new file mode 100644 index 0000000..f1012f9 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple.java @@ -0,0 +1,71 @@ +package org.petitparser.utils.tuples; + +import java.util.List; + +/** + * Common interface and static factory for strongly-typed tuples. + */ +public interface Tuple extends List { + + /** + * Creates a 2-tuple. + */ + static Tuple2 of(T1 first, T2 second) { + return new Tuple2<>(first, second); + } + + /** + * Creates a 3-tuple. + */ + static Tuple3 of(T1 first, T2 second, T3 third) { + return new Tuple3<>(first, second, third); + } + + /** + * Creates a 4-tuple. + */ + static Tuple4 of( + T1 first, T2 second, T3 third, T4 fourth) { + return new Tuple4<>(first, second, third, fourth); + } + + /** + * Creates a 5-tuple. + */ + static Tuple5 of( + T1 first, T2 second, T3 third, T4 fourth, T5 fifth) { + return new Tuple5<>(first, second, third, fourth, fifth); + } + + /** + * Creates a 6-tuple. + */ + static Tuple6 of( + T1 first, T2 second, T3 third, T4 fourth, T5 fifth, T6 sixth) { + return new Tuple6<>(first, second, third, fourth, fifth, sixth); + } + + /** + * Creates a 7-tuple. + */ + static Tuple7 of( + T1 first, T2 second, T3 third, T4 fourth, T5 fifth, T6 sixth, T7 seventh) { + return new Tuple7<>(first, second, third, fourth, fifth, sixth, seventh); + } + + /** + * Creates a 8-tuple. + */ + static Tuple8 of( + T1 first, T2 second, T3 third, T4 fourth, T5 fifth, T6 sixth, T7 seventh, T8 eighth) { + return new Tuple8<>(first, second, third, fourth, fifth, sixth, seventh, eighth); + } + + /** + * Creates a 9-tuple. + */ + static Tuple9 of( + T1 first, T2 second, T3 third, T4 fourth, T5 fifth, T6 sixth, T7 seventh, T8 eighth, T9 ninth) { + return new Tuple9<>(first, second, third, fourth, fifth, sixth, seventh, eighth, ninth); + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple2.java b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple2.java new file mode 100644 index 0000000..0edbef5 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple2.java @@ -0,0 +1,74 @@ +package org.petitparser.utils.tuples; + +import java.util.AbstractList; +import java.util.List; +import java.util.Objects; + +/** + * An immutable 2-element tuple extending {@link AbstractList}. + * + * @param the type of the first element + * @param the type of the second element + */ +public class Tuple2 extends AbstractList implements Tuple { + + protected final T1 first; + protected final T2 second; + + public Tuple2(T1 first, T2 second) { + this.first = first; + this.second = second; + } + + public T1 first() { + return first; + } + + public T2 second() { + return second; + } + + @Override + public Object get(int index) { + return switch (index) { + case 0 -> first; + case 1 -> second; + default -> throw new IndexOutOfBoundsException("Index: " + index + ", Size: 2"); + }; + } + + @Override + public int size() { + return 2; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof Tuple2 that) { + return Objects.equals(first, that.first) && + Objects.equals(second, that.second); + } + if (!(other instanceof List that)) { + return false; + } + return that.size() == 2 && + Objects.equals(first, that.get(0)) && + Objects.equals(second, that.get(1)); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (first == null ? 0 : first.hashCode()); + result = 31 * result + (second == null ? 0 : second.hashCode()); + return result; + } + + @Override + public String toString() { + return "[" + first + ", " + second + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple3.java b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple3.java new file mode 100644 index 0000000..1f76a55 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple3.java @@ -0,0 +1,85 @@ +package org.petitparser.utils.tuples; + +import java.util.AbstractList; +import java.util.List; +import java.util.Objects; + +/** + * An immutable 3-element tuple extending {@link AbstractList}. + * + * @param the type of the first element + * @param the type of the second element + * @param the type of the third element + */ +public class Tuple3 extends AbstractList implements Tuple { + + protected final T1 first; + protected final T2 second; + protected final T3 third; + + public Tuple3(T1 first, T2 second, T3 third) { + this.first = first; + this.second = second; + this.third = third; + } + + public T1 first() { + return first; + } + + public T2 second() { + return second; + } + + public T3 third() { + return third; + } + + @Override + public Object get(int index) { + return switch (index) { + case 0 -> first; + case 1 -> second; + case 2 -> third; + default -> throw new IndexOutOfBoundsException("Index: " + index + ", Size: 3"); + }; + } + + @Override + public int size() { + return 3; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof Tuple3 that) { + return Objects.equals(first, that.first) && + Objects.equals(second, that.second) && + Objects.equals(third, that.third); + } + if (!(other instanceof List that)) { + return false; + } + return that.size() == 3 && + Objects.equals(first, that.get(0)) && + Objects.equals(second, that.get(1)) && + Objects.equals(third, that.get(2)); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (first == null ? 0 : first.hashCode()); + result = 31 * result + (second == null ? 0 : second.hashCode()); + result = 31 * result + (third == null ? 0 : third.hashCode()); + return result; + } + + @Override + public String toString() { + return "[" + first + ", " + second + ", " + third + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple4.java b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple4.java new file mode 100644 index 0000000..cefb979 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple4.java @@ -0,0 +1,96 @@ +package org.petitparser.utils.tuples; + +import java.util.AbstractList; +import java.util.List; +import java.util.Objects; + +/** + * An immutable 4-element tuple extending {@link AbstractList}. + * + * @param the type of the first element + * @param the type of the second element + * @param the type of the third element + * @param the type of the fourth element + */ +public class Tuple4 extends AbstractList implements Tuple { + + protected final T1 first; + protected final T2 second; + protected final T3 third; + protected final T4 fourth; + + public Tuple4(T1 first, T2 second, T3 third, T4 fourth) { + this.first = first; + this.second = second; + this.third = third; + this.fourth = fourth; + } + + public T1 first() { + return first; + } + + public T2 second() { + return second; + } + + public T3 third() { + return third; + } + + public T4 fourth() { + return fourth; + } + + @Override + public Object get(int index) { + return switch (index) { + case 0 -> first; + case 1 -> second; + case 2 -> third; + case 3 -> fourth; + default -> throw new IndexOutOfBoundsException("Index: " + index + ", Size: 4"); + }; + } + + @Override + public int size() { + return 4; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof Tuple4 that) { + return Objects.equals(first, that.first) && + Objects.equals(second, that.second) && + Objects.equals(third, that.third) && + Objects.equals(fourth, that.fourth); + } + if (!(other instanceof List that)) { + return false; + } + return that.size() == 4 && + Objects.equals(first, that.get(0)) && + Objects.equals(second, that.get(1)) && + Objects.equals(third, that.get(2)) && + Objects.equals(fourth, that.get(3)); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (first == null ? 0 : first.hashCode()); + result = 31 * result + (second == null ? 0 : second.hashCode()); + result = 31 * result + (third == null ? 0 : third.hashCode()); + result = 31 * result + (fourth == null ? 0 : fourth.hashCode()); + return result; + } + + @Override + public String toString() { + return "[" + first + ", " + second + ", " + third + ", " + fourth + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple5.java b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple5.java new file mode 100644 index 0000000..402f31f --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple5.java @@ -0,0 +1,107 @@ +package org.petitparser.utils.tuples; + +import java.util.AbstractList; +import java.util.List; +import java.util.Objects; + +/** + * An immutable 5-element tuple extending {@link AbstractList}. + * + * @param the type of the first element + * @param the type of the second element + * @param the type of the third element + * @param the type of the fourth element + * @param the type of the fifth element + */ +public class Tuple5 extends AbstractList implements Tuple { + + protected final T1 first; + protected final T2 second; + protected final T3 third; + protected final T4 fourth; + protected final T5 fifth; + + public Tuple5(T1 first, T2 second, T3 third, T4 fourth, T5 fifth) { + this.first = first; + this.second = second; + this.third = third; + this.fourth = fourth; + this.fifth = fifth; + } + + public T1 first() { + return first; + } + + public T2 second() { + return second; + } + + public T3 third() { + return third; + } + + public T4 fourth() { + return fourth; + } + + public T5 fifth() { + return fifth; + } + + @Override + public Object get(int index) { + return switch (index) { + case 0 -> first; + case 1 -> second; + case 2 -> third; + case 3 -> fourth; + case 4 -> fifth; + default -> throw new IndexOutOfBoundsException("Index: " + index + ", Size: 5"); + }; + } + + @Override + public int size() { + return 5; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof Tuple5 that) { + return Objects.equals(first, that.first) && + Objects.equals(second, that.second) && + Objects.equals(third, that.third) && + Objects.equals(fourth, that.fourth) && + Objects.equals(fifth, that.fifth); + } + if (!(other instanceof List that)) { + return false; + } + return that.size() == 5 && + Objects.equals(first, that.get(0)) && + Objects.equals(second, that.get(1)) && + Objects.equals(third, that.get(2)) && + Objects.equals(fourth, that.get(3)) && + Objects.equals(fifth, that.get(4)); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (first == null ? 0 : first.hashCode()); + result = 31 * result + (second == null ? 0 : second.hashCode()); + result = 31 * result + (third == null ? 0 : third.hashCode()); + result = 31 * result + (fourth == null ? 0 : fourth.hashCode()); + result = 31 * result + (fifth == null ? 0 : fifth.hashCode()); + return result; + } + + @Override + public String toString() { + return "[" + first + ", " + second + ", " + third + ", " + fourth + ", " + fifth + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple6.java b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple6.java new file mode 100644 index 0000000..e564ebe --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple6.java @@ -0,0 +1,118 @@ +package org.petitparser.utils.tuples; + +import java.util.AbstractList; +import java.util.List; +import java.util.Objects; + +/** + * An immutable 6-element tuple extending {@link AbstractList}. + * + * @param the type of the first element + * @param the type of the second element + * @param the type of the third element + * @param the type of the fourth element + * @param the type of the fifth element + * @param the type of the sixth element + */ +public class Tuple6 extends AbstractList implements Tuple { + + protected final T1 first; + protected final T2 second; + protected final T3 third; + protected final T4 fourth; + protected final T5 fifth; + protected final T6 sixth; + + public Tuple6(T1 first, T2 second, T3 third, T4 fourth, T5 fifth, T6 sixth) { + this.first = first; + this.second = second; + this.third = third; + this.fourth = fourth; + this.fifth = fifth; + this.sixth = sixth; + } + + public T1 first() { + return first; + } + + public T2 second() { + return second; + } + + public T3 third() { + return third; + } + + public T4 fourth() { + return fourth; + } + + public T5 fifth() { + return fifth; + } + + public T6 sixth() { + return sixth; + } + + @Override + public Object get(int index) { + return switch (index) { + case 0 -> first; + case 1 -> second; + case 2 -> third; + case 3 -> fourth; + case 4 -> fifth; + case 5 -> sixth; + default -> throw new IndexOutOfBoundsException("Index: " + index + ", Size: 6"); + }; + } + + @Override + public int size() { + return 6; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof Tuple6 that) { + return Objects.equals(first, that.first) && + Objects.equals(second, that.second) && + Objects.equals(third, that.third) && + Objects.equals(fourth, that.fourth) && + Objects.equals(fifth, that.fifth) && + Objects.equals(sixth, that.sixth); + } + if (!(other instanceof List that)) { + return false; + } + return that.size() == 6 && + Objects.equals(first, that.get(0)) && + Objects.equals(second, that.get(1)) && + Objects.equals(third, that.get(2)) && + Objects.equals(fourth, that.get(3)) && + Objects.equals(fifth, that.get(4)) && + Objects.equals(sixth, that.get(5)); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (first == null ? 0 : first.hashCode()); + result = 31 * result + (second == null ? 0 : second.hashCode()); + result = 31 * result + (third == null ? 0 : third.hashCode()); + result = 31 * result + (fourth == null ? 0 : fourth.hashCode()); + result = 31 * result + (fifth == null ? 0 : fifth.hashCode()); + result = 31 * result + (sixth == null ? 0 : sixth.hashCode()); + return result; + } + + @Override + public String toString() { + return "[" + first + ", " + second + ", " + third + ", " + fourth + ", " + fifth + ", " + sixth + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple7.java b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple7.java new file mode 100644 index 0000000..2493135 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple7.java @@ -0,0 +1,130 @@ +package org.petitparser.utils.tuples; + +import java.util.AbstractList; +import java.util.List; +import java.util.Objects; + +/** + * An immutable 7-element tuple extending {@link AbstractList}. + * + * @param the type of the first element + * @param the type of the second element + * @param the type of the third element + * @param the type of the fourth element + * @param the type of the fifth element + * @param the type of the sixth element + * @param the type of the seventh element + */ +public class Tuple7 extends AbstractList implements Tuple { + + protected final T1 first; + protected final T2 second; + protected final T3 third; + protected final T4 fourth; + protected final T5 fifth; + protected final T6 sixth; + protected final T7 seventh; + + public Tuple7( + T1 first, T2 second, T3 third, T4 fourth, T5 fifth, T6 sixth, T7 seventh) { + this.first = first; + this.second = second; + this.third = third; + this.fourth = fourth; + this.fifth = fifth; + this.sixth = sixth; + this.seventh = seventh; + } + + public T1 first() { + return first; + } + + public T2 second() { + return second; + } + + public T3 third() { + return third; + } + + public T4 fourth() { + return fourth; + } + + public T5 fifth() { + return fifth; + } + + public T6 sixth() { + return sixth; + } + + public T7 seventh() { + return seventh; + } + + @Override + public Object get(int index) { + return switch (index) { + case 0 -> first; + case 1 -> second; + case 2 -> third; + case 3 -> fourth; + case 4 -> fifth; + case 5 -> sixth; + case 6 -> seventh; + default -> throw new IndexOutOfBoundsException("Index: " + index + ", Size: 7"); + }; + } + + @Override + public int size() { + return 7; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof Tuple7 that) { + return Objects.equals(first, that.first) && + Objects.equals(second, that.second) && + Objects.equals(third, that.third) && + Objects.equals(fourth, that.fourth) && + Objects.equals(fifth, that.fifth) && + Objects.equals(sixth, that.sixth) && + Objects.equals(seventh, that.seventh); + } + if (!(other instanceof List that)) { + return false; + } + return that.size() == 7 && + Objects.equals(first, that.get(0)) && + Objects.equals(second, that.get(1)) && + Objects.equals(third, that.get(2)) && + Objects.equals(fourth, that.get(3)) && + Objects.equals(fifth, that.get(4)) && + Objects.equals(sixth, that.get(5)) && + Objects.equals(seventh, that.get(6)); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (first == null ? 0 : first.hashCode()); + result = 31 * result + (second == null ? 0 : second.hashCode()); + result = 31 * result + (third == null ? 0 : third.hashCode()); + result = 31 * result + (fourth == null ? 0 : fourth.hashCode()); + result = 31 * result + (fifth == null ? 0 : fifth.hashCode()); + result = 31 * result + (sixth == null ? 0 : sixth.hashCode()); + result = 31 * result + (seventh == null ? 0 : seventh.hashCode()); + return result; + } + + @Override + public String toString() { + return "[" + first + ", " + second + ", " + third + ", " + fourth + ", " + fifth + ", " + sixth + ", " + seventh + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple8.java b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple8.java new file mode 100644 index 0000000..4aa9429 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple8.java @@ -0,0 +1,143 @@ +package org.petitparser.utils.tuples; + +import java.util.AbstractList; +import java.util.List; +import java.util.Objects; + +/** + * An immutable 8-element tuple extending {@link AbstractList}. + * + * @param the type of the first element + * @param the type of the second element + * @param the type of the third element + * @param the type of the fourth element + * @param the type of the fifth element + * @param the type of the sixth element + * @param the type of the seventh element + * @param the type of the eighth element + */ +public class Tuple8 extends AbstractList implements Tuple { + + protected final T1 first; + protected final T2 second; + protected final T3 third; + protected final T4 fourth; + protected final T5 fifth; + protected final T6 sixth; + protected final T7 seventh; + protected final T8 eighth; + + public Tuple8( + T1 first, T2 second, T3 third, T4 fourth, + T5 fifth, T6 sixth, T7 seventh, T8 eighth) { + this.first = first; + this.second = second; + this.third = third; + this.fourth = fourth; + this.fifth = fifth; + this.sixth = sixth; + this.seventh = seventh; + this.eighth = eighth; + } + + public T1 first() { + return first; + } + + public T2 second() { + return second; + } + + public T3 third() { + return third; + } + + public T4 fourth() { + return fourth; + } + + public T5 fifth() { + return fifth; + } + + public T6 sixth() { + return sixth; + } + + public T7 seventh() { + return seventh; + } + + public T8 eighth() { + return eighth; + } + + @Override + public Object get(int index) { + return switch (index) { + case 0 -> first; + case 1 -> second; + case 2 -> third; + case 3 -> fourth; + case 4 -> fifth; + case 5 -> sixth; + case 6 -> seventh; + case 7 -> eighth; + default -> throw new IndexOutOfBoundsException("Index: " + index + ", Size: 8"); + }; + } + + @Override + public int size() { + return 8; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof Tuple8 that) { + return Objects.equals(first, that.first) && + Objects.equals(second, that.second) && + Objects.equals(third, that.third) && + Objects.equals(fourth, that.fourth) && + Objects.equals(fifth, that.fifth) && + Objects.equals(sixth, that.sixth) && + Objects.equals(seventh, that.seventh) && + Objects.equals(eighth, that.eighth); + } + if (!(other instanceof List that)) { + return false; + } + return that.size() == 8 && + Objects.equals(first, that.get(0)) && + Objects.equals(second, that.get(1)) && + Objects.equals(third, that.get(2)) && + Objects.equals(fourth, that.get(3)) && + Objects.equals(fifth, that.get(4)) && + Objects.equals(sixth, that.get(5)) && + Objects.equals(seventh, that.get(6)) && + Objects.equals(eighth, that.get(7)); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (first == null ? 0 : first.hashCode()); + result = 31 * result + (second == null ? 0 : second.hashCode()); + result = 31 * result + (third == null ? 0 : third.hashCode()); + result = 31 * result + (fourth == null ? 0 : fourth.hashCode()); + result = 31 * result + (fifth == null ? 0 : fifth.hashCode()); + result = 31 * result + (sixth == null ? 0 : sixth.hashCode()); + result = 31 * result + (seventh == null ? 0 : seventh.hashCode()); + result = 31 * result + (eighth == null ? 0 : eighth.hashCode()); + return result; + } + + @Override + public String toString() { + return "[" + first + ", " + second + ", " + third + ", " + fourth + ", " + + fifth + ", " + sixth + ", " + seventh + ", " + eighth + "]"; + } +} diff --git a/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple9.java b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple9.java new file mode 100644 index 0000000..334e065 --- /dev/null +++ b/petitparser-core/src/main/java/org/petitparser/utils/tuples/Tuple9.java @@ -0,0 +1,154 @@ +package org.petitparser.utils.tuples; + +import java.util.AbstractList; +import java.util.List; +import java.util.Objects; + +/** + * An immutable 9-element tuple extending {@link AbstractList}. + * + * @param the type of the first element + * @param the type of the second element + * @param the type of the third element + * @param the type of the fourth element + * @param the type of the fifth element + * @param the type of the sixth element + * @param the type of the seventh element + * @param the type of the eighth element + * @param the type of the ninth element + */ +public class Tuple9 extends AbstractList implements Tuple { + + protected final T1 first; + protected final T2 second; + protected final T3 third; + protected final T4 fourth; + protected final T5 fifth; + protected final T6 sixth; + protected final T7 seventh; + protected final T8 eighth; + protected final T9 ninth; + + public Tuple9( + T1 first, T2 second, T3 third, T4 fourth, T5 fifth, + T6 sixth, T7 seventh, T8 eighth, T9 ninth) { + this.first = first; + this.second = second; + this.third = third; + this.fourth = fourth; + this.fifth = fifth; + this.sixth = sixth; + this.seventh = seventh; + this.eighth = eighth; + this.ninth = ninth; + } + + public T1 first() { + return first; + } + + public T2 second() { + return second; + } + + public T3 third() { + return third; + } + + public T4 fourth() { + return fourth; + } + + public T5 fifth() { + return fifth; + } + + public T6 sixth() { + return sixth; + } + + public T7 seventh() { + return seventh; + } + + public T8 eighth() { + return eighth; + } + + public T9 ninth() { + return ninth; + } + + @Override + public Object get(int index) { + return switch (index) { + case 0 -> first; + case 1 -> second; + case 2 -> third; + case 3 -> fourth; + case 4 -> fifth; + case 5 -> sixth; + case 6 -> seventh; + case 7 -> eighth; + case 8 -> ninth; + default -> throw new IndexOutOfBoundsException("Index: " + index + ", Size: 9"); + }; + } + + @Override + public int size() { + return 9; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other instanceof Tuple9 that) { + return Objects.equals(first, that.first) && + Objects.equals(second, that.second) && + Objects.equals(third, that.third) && + Objects.equals(fourth, that.fourth) && + Objects.equals(fifth, that.fifth) && + Objects.equals(sixth, that.sixth) && + Objects.equals(seventh, that.seventh) && + Objects.equals(eighth, that.eighth) && + Objects.equals(ninth, that.ninth); + } + if (!(other instanceof List that)) { + return false; + } + return that.size() == 9 && + Objects.equals(first, that.get(0)) && + Objects.equals(second, that.get(1)) && + Objects.equals(third, that.get(2)) && + Objects.equals(fourth, that.get(3)) && + Objects.equals(fifth, that.get(4)) && + Objects.equals(sixth, that.get(5)) && + Objects.equals(seventh, that.get(6)) && + Objects.equals(eighth, that.get(7)) && + Objects.equals(ninth, that.get(8)); + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (first == null ? 0 : first.hashCode()); + result = 31 * result + (second == null ? 0 : second.hashCode()); + result = 31 * result + (third == null ? 0 : third.hashCode()); + result = 31 * result + (fourth == null ? 0 : fourth.hashCode()); + result = 31 * result + (fifth == null ? 0 : fifth.hashCode()); + result = 31 * result + (sixth == null ? 0 : sixth.hashCode()); + result = 31 * result + (seventh == null ? 0 : seventh.hashCode()); + result = 31 * result + (eighth == null ? 0 : eighth.hashCode()); + result = 31 * result + (ninth == null ? 0 : ninth.hashCode()); + return result; + } + + @Override + public String toString() { + return "[" + first + ", " + second + ", " + third + ", " + fourth + ", " + + fifth + ", " + sixth + ", " + seventh + ", " + eighth + ", " + ninth + "]"; + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/Assertions.java b/petitparser-core/src/test/java/org/petitparser/Assertions.java index 39ecf4a..1fd4bc4 100644 --- a/petitparser-core/src/test/java/org/petitparser/Assertions.java +++ b/petitparser-core/src/test/java/org/petitparser/Assertions.java @@ -6,13 +6,26 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class Assertions { + public static void assertAccept(Parser parser, String input) { + assertAccept(parser, input, input.length()); + } + + public static void assertAccept(Parser parser, String input, int position) { + Result result = parser.parse(input); + String resultToString = result.toString(); + assertTrue("Expected parse success, but got " + resultToString, result.isSuccess()); + assertEquals("Position", position, result.getPosition()); + assertNull("No message expected", result.getMessage()); + assertEquals("Fast parse", position, parser.fastParseOn(input, 0)); + assertTrue("Accept", parser.accept(input)); + } + public static void assertSuccess(Parser parser, String input, T result) { assertSuccess(parser, input, result, input.length()); } @@ -20,9 +33,8 @@ public static void assertSuccess(Parser parser, String input, T result) { public static void assertSuccess( Parser parser, String input, T expected, int position) { Result result = parser.parse(input); - assertNotNull(result.toString()); - assertTrue("Expected parse success", result.isSuccess()); - assertFalse("Expected parse success", result.isFailure()); + String resultToString = result.toString(); + assertTrue("Expected parse success, but got " + resultToString, result.isSuccess()); assertEquals("Position", position, result.getPosition()); assertEquals("Result", expected, result.get()); assertNull("No message expected", result.getMessage()); @@ -47,9 +59,8 @@ public static void assertFailure( public static void assertFailure( Parser parser, String input, int position, String message) { Result result = parser.parse(input); - assertNotNull(result.toString()); - assertFalse("Expected parse failure", result.isSuccess()); - assertTrue("Expected parse failure", result.isFailure()); + String resultToString = result.toString(); + assertTrue("Expected parse failure, but got " + resultToString, result.isFailure()); assertEquals("Position", position, result.getPosition()); if (message != null) { assertEquals("Message expected", message, result.getMessage()); diff --git a/petitparser-core/src/test/java/org/petitparser/CharacterTest.java b/petitparser-core/src/test/java/org/petitparser/CharacterTest.java index ce8813b..d5f8c5a 100644 --- a/petitparser-core/src/test/java/org/petitparser/CharacterTest.java +++ b/petitparser-core/src/test/java/org/petitparser/CharacterTest.java @@ -2,7 +2,6 @@ import org.junit.Test; import org.petitparser.parser.Parser; -import org.petitparser.parser.primitive.CharacterParser; import static org.petitparser.Assertions.assertFailure; import static org.petitparser.Assertions.assertSuccess; @@ -421,4 +420,11 @@ public void testWordWithMessage() { assertFailure(parser, "-", "wrong"); assertFailure(parser, "", "wrong"); } + + @Test + public void testPatternUnicodeCaseInsensitive() { + Parser parser = org.petitparser.parser.primitive.CharacterParser.pattern("A-Z", "error", true, true); + assertSuccess(parser, "a", "a"); + assertSuccess(parser, "A", "A"); + } } diff --git a/petitparser-core/src/test/java/org/petitparser/EqualityTest.java b/petitparser-core/src/test/java/org/petitparser/EqualityTest.java index 7756890..af4ba48 100644 --- a/petitparser-core/src/test/java/org/petitparser/EqualityTest.java +++ b/petitparser-core/src/test/java/org/petitparser/EqualityTest.java @@ -11,8 +11,7 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Function; - -import static junit.framework.TestCase.assertFalse; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; @@ -113,6 +112,11 @@ public void epsilon() { verify(new EpsilonParser()); } + @Test + public void epsilonWithValue() { + verify(new EpsilonParser(42)); + } + @Test public void failure() { verify(FailureParser.withMessage("failure")); @@ -128,11 +132,21 @@ public void flatten2() { verify(CharacterParser.digit().flatten("digit")); } + @Test + public void labeled() { + verify(CharacterParser.digit().labeled("digit")); + } + @Test public void map() { verify(CharacterParser.digit().map(Function.identity())); } + @Test + public void newline() { + verify(Parser.newline()); + } + @Test public void not() { verify(CharacterParser.digit().not()); @@ -163,6 +177,11 @@ public void plusLazy() { verify(CharacterParser.digit().plusLazy(CharacterParser.word())); } + @Test + public void position() { + verify(Parser.position()); + } + @Test public void repeat() { verify(CharacterParser.digit().repeat(2, 3)); @@ -228,4 +247,76 @@ public void trim() { verify(CharacterParser.digit() .trim(CharacterParser.of('a'), CharacterParser.of('b'))); } + + @Test + public void skip() { + verify(CharacterParser.digit() + .skip(CharacterParser.of('['), CharacterParser.of(']'))); + } + + @Test + public void where() { + verify(CharacterParser.digit().where((Character c) -> Character.isDigit(c))); + } + + @Test + public void whereWithMessage() { + verify(CharacterParser.digit() + .where((Character c) -> Character.isDigit(c), "digit expected")); + } + + @Test + public void repeatingCharacter() { + verify(CharacterParser.digit().plusString()); + } + + @Test + public void starSeparated() { + verify(CharacterParser.digit().starSeparated(CharacterParser.of(','))); + } + + @Test + public void plusSeparated() { + verify(CharacterParser.digit().plusSeparated(CharacterParser.of(','))); + } + + @Test + public void timesSeparated() { + verify(CharacterParser.digit().timesSeparated(CharacterParser.of(','), 3)); + } + + @Test + public void repeatSeparated() { + verify(CharacterParser.digit().repeatSeparated(CharacterParser.of(','), 2, 4)); + } + + @Test + public void cast() { + verify(CharacterParser.digit().cast()); + } + + @Test + public void castWithClass() { + verify(CharacterParser.digit().cast(Character.class)); + } + + @Test + public void castList() { + verify(CharacterParser.digit().star().castList()); + } + + @Test + public void castListWithClass() { + verify(CharacterParser.digit().star().castList(Character.class)); + } + + @Test + public void pick() { + verify(CharacterParser.digit().star().pick(1)); + } + + @Test + public void permute() { + verify(CharacterParser.digit().star().permute(1, 0)); + } } diff --git a/petitparser-core/src/test/java/org/petitparser/ExamplesTest.java b/petitparser-core/src/test/java/org/petitparser/ExamplesTest.java index bb34bfb..0fcee97 100644 --- a/petitparser-core/src/test/java/org/petitparser/ExamplesTest.java +++ b/petitparser-core/src/test/java/org/petitparser/ExamplesTest.java @@ -1,12 +1,19 @@ package org.petitparser; import org.junit.Test; +import org.petitparser.context.Result; import org.petitparser.parser.Parser; import org.petitparser.parser.combinators.SettableParser; +import org.petitparser.parser.repeating.SeparatedList; import org.petitparser.tools.ExpressionBuilder; +import java.util.Arrays; import java.util.List; import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.petitparser.Assertions.assertFailure; import static org.petitparser.Assertions.assertSuccess; @@ -193,7 +200,7 @@ public void testExpressionBuilderWithSettableExample() throws Exception { .seq(recursion) .seq(of(')')).map((List values) -> values.get(1)); - ExpressionBuilder builder = new ExpressionBuilder(); + ExpressionBuilder builder = new ExpressionBuilder<>(); builder.group() .primitive(bracket.or(DOUBLE)); @@ -207,7 +214,7 @@ public void testExpressionBuilderWithSettableExample() throws Exception { @Test public void testExpressionBuilderWithWrapperExample() throws Exception { - ExpressionBuilder builder = new ExpressionBuilder(); + ExpressionBuilder builder = new ExpressionBuilder<>(); builder.group() .primitive(DOUBLE) .wrapper(of('(').trim(), of(')').trim(), @@ -219,7 +226,7 @@ public void testExpressionBuilderWithWrapperExample() throws Exception { assertCalculatorExample(parser); } - private void initOperators(ExpressionBuilder builder) { + private void initOperators(ExpressionBuilder builder) { // negation is a prefix operator builder.group() .prefix(of('-').trim(), (List values) -> -values.get(1)); @@ -250,4 +257,68 @@ private void assertCalculatorExample(Parser parser) { assertSuccess(intCalculator, "8/4/2", 1); assertSuccess(intCalculator, "2^2^3", 256); } + + @Test + public void testModernExpressionBuilder() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.group() + .primitive(DOUBLE) + .wrapper(of('(').trim(), of(')').trim(), (left, value, right) -> value); + + // negation is a prefix operator + builder.group() + .prefix(of('-').trim(), (op, value) -> -value); + + // power is right-associative + builder.group() + .right(of('^').trim(), (left, op, right) -> Math.pow(left, right)); + + // multiplication and division are left-associative + builder.group() + .left(of('*').trim(), (left, op, right) -> left * right) + .left(of('/').trim(), (left, op, right) -> left / right); + + // addition and subtraction are left-associative + builder.group() + .left(of('+').trim(), (left, op, right) -> left + right) + .left(of('-').trim(), (left, op, right) -> left - right); + + Parser parser = builder.build().end(); + assertCalculatorExample(parser); + } + + @Test + public void testTypedSequences() { + Parser parser = letter().then(digit().plusString()) + .map((ch, num) -> ch + ":" + num); + assertSuccess(parser, "a123", "a:123"); + } + + @Test + public void testSkip() { + Parser parser = DOUBLE.skip(of('(').trim(), of(')').trim()); + assertSuccess(parser, "( 42.5 )", 42.5); + } + + @Test + public void testStarSeparated() { + Parser parser = DOUBLE.starSeparated(of(',').trim()); + Result result = parser.parse("1.5, 2.5, 3.5"); + assertTrue(result.isSuccess()); + SeparatedList list = result.get(); + assertEquals(Arrays.asList(1.5, 2.5, 3.5), list.getElements()); + } + + @Test + public void testMatchesAsStream() { + List allMatches = letter().plusString() + .matchesAsStream("one 123 two") + .collect(Collectors.toList()); + assertEquals(Arrays.asList("one", "ne", "e", "two", "wo", "o"), allMatches); + + List nonOverlapping = letter().plusString() + .matchesSkippingAsStream("one 123 two 456 three") + .collect(Collectors.toList()); + assertEquals(Arrays.asList("one", "two", "three"), nonOverlapping); + } } diff --git a/petitparser-core/src/test/java/org/petitparser/MatcherTest.java b/petitparser-core/src/test/java/org/petitparser/MatcherTest.java new file mode 100644 index 0000000..9384363 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/MatcherTest.java @@ -0,0 +1,331 @@ +package org.petitparser; + +import org.junit.Test; +import org.petitparser.parser.MatchesIterable; +import org.petitparser.parser.MatchesIterator; +import org.petitparser.parser.MatchesSpliterator; +import org.petitparser.parser.Parser; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.of; +import static org.petitparser.parser.primitive.EpsilonParser.epsilon; + +/** + * Tests for matcher extensions on {@link Parser} and lazy matching structures. + */ +public class MatcherTest { + + @Test + public void testParse() { + Parser parser = of('a'); + assertTrue(parser.parse("a").isSuccess()); + assertFalse(parser.parse("b").isSuccess()); + } + + @Test + public void testParseWithStart() { + Parser parser = of('b'); + assertFalse(parser.parse("abc", 0).isSuccess()); + assertTrue(parser.parse("abc", 1).isSuccess()); + assertFalse(parser.parse("abc", 2).isSuccess()); + assertFalse(parser.parse("abc", 3).isSuccess()); + assertFalse(parser.parse("abc", 4).isSuccess()); + assertFalse(parser.parse("abc", -1).isSuccess()); + } + + @Test + public void testAccept() { + Parser parser = of('a'); + assertTrue(parser.accept("a")); + assertFalse(parser.accept("b")); + } + + @Test + public void testAcceptWithStart() { + Parser parser = of('b'); + assertFalse(parser.accept("abc", 0)); + assertTrue(parser.accept("abc", 1)); + assertFalse(parser.accept("abc", 2)); + assertFalse(parser.accept("abc", 3)); + assertFalse(parser.accept("abc", 4)); + assertFalse(parser.accept("abc", -1)); + assertFalse(parser.accept(null, 0)); + } + + @Test + public void testAcceptEpsilonAtEnd() { + Parser parser = epsilon(); + assertTrue(parser.accept("abc", 3)); + assertFalse(parser.accept("abc", 4)); + } + + @Test + public void testAllMatchesOverlapping() { + String input = "a123b456"; + Parser parser = digit().seq(digit()).flatten(); + + List expected = Arrays.asList("12", "23", "45", "56"); + List actual = parser.matches(input); + assertEquals(expected, actual); + + List actualFromStart0 = new ArrayList<>(); + parser.matches(input, 0).forEach(actualFromStart0::add); + assertEquals(expected, actualFromStart0); + + List actualFromStart3 = new ArrayList<>(); + parser.matches(input, 3).forEach(actualFromStart3::add); + assertEquals(Arrays.asList("45", "56"), actualFromStart3); + } + + @Test + public void testAllMatchesNonOverlapping() { + String input = "a123b456"; + Parser parser = digit().seq(digit()).flatten(); + + List expected = Arrays.asList("12", "45"); + List actual = parser.matchesSkipping(input); + assertEquals(expected, actual); + + List actualFromStart0 = new ArrayList<>(); + parser.matchesSkipping(input, 0).forEach(actualFromStart0::add); + assertEquals(expected, actualFromStart0); + + List actualFromStart3 = new ArrayList<>(); + parser.matchesSkipping(input, 3).forEach(actualFromStart3::add); + assertEquals(Collections.singletonList("45"), actualFromStart3); + } + + @Test + public void testMatchesEmptyMatch() { + String input = "a123b45"; + Parser parser = digit().star().flatten(); + + List actualNonOverlapping = new ArrayList<>(); + parser.matches(input, 0, false).forEach(actualNonOverlapping::add); + assertEquals(Arrays.asList("", "123", "", "45", ""), actualNonOverlapping); + } + + @Test + public void testMatchesBeyondLength() { + Parser parser = digit(); + List actual = new ArrayList<>(); + parser.matches("123", 10).forEach(actual::add); + assertTrue(actual.isEmpty()); + + List actualSkipping = new ArrayList<>(); + parser.matchesSkipping("123", 10).forEach(actualSkipping::add); + assertTrue(actualSkipping.isEmpty()); + } + + @Test + public void testMatchesStreamLazy() { + AtomicInteger invocationCount = new AtomicInteger(0); + Parser parser = digit().seq(digit()).flatten() + .map(val -> { + invocationCount.incrementAndGet(); + return val; + }); + + Stream stream = parser.matchesAsStream("a123b456"); + // Stream created, no elements evaluated yet + assertEquals(0, invocationCount.get()); + + String first = stream.findFirst().orElse(null); + assertEquals("12", first); + // Only the first match should have been evaluated (1 instead of 4) + assertEquals(1, invocationCount.get()); + } + + @Test + public void testMatchesAsStreamOverloads() { + String input = "a123b456"; + Parser parser = digit().seq(digit()).flatten(); + + List overlapping0 = parser.matchesAsStream(input) + .collect(Collectors.toList()); + assertEquals(Arrays.asList("12", "23", "45", "56"), overlapping0); + + List overlapping3 = parser.matchesAsStream(input, 3) + .collect(Collectors.toList()); + assertEquals(Arrays.asList("45", "56"), overlapping3); + + List nonOverlapping0 = parser.matchesSkippingAsStream(input) + .collect(Collectors.toList()); + assertEquals(Arrays.asList("12", "45"), nonOverlapping0); + + List nonOverlapping3 = parser.matchesSkippingAsStream(input, 3) + .collect(Collectors.toList()); + assertEquals(Collections.singletonList("45"), nonOverlapping3); + } + + @Test + public void testMatchesSpliteratorDirectly() { + Parser parser = digit().seq(digit()).flatten(); + String input = "a123b45"; + MatchesSpliterator spliterator = + new MatchesSpliterator<>(parser, input, 1, false); + + assertEquals(parser, spliterator.getParser()); + assertEquals(input, spliterator.getInput()); + assertEquals(1, spliterator.getStart()); + assertFalse(spliterator.isOverlapping()); + assertNull(spliterator.trySplit()); + assertEquals(Long.MAX_VALUE, spliterator.estimateSize()); + assertEquals(Spliterator.ORDERED, spliterator.characteristics()); + assertTrue(spliterator.toString().contains("MatchesSpliterator")); + + List collected = new ArrayList<>(); + spliterator.forEachRemaining(collected::add); + assertEquals(Arrays.asList("12", "45"), collected); + } + + @Test + public void testMatchesIteratorDirectly() { + Parser parser = of('x'); + String input = "x_x_x"; + MatchesIterator iterator = + new MatchesIterator<>(parser, input, 0, false); + + assertEquals(parser, iterator.getParser()); + assertEquals(input, iterator.getInput()); + assertEquals(0, iterator.getStart()); + assertFalse(iterator.isOverlapping()); + assertTrue(iterator.toString().contains("MatchesIterator")); + + assertTrue(iterator.hasNext()); + assertEquals(Character.valueOf('x'), iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals(Character.valueOf('x'), iterator.next()); + assertTrue(iterator.hasNext()); + assertEquals(Character.valueOf('x'), iterator.next()); + assertFalse(iterator.hasNext()); + + try { + iterator.next(); + org.junit.Assert.fail("Expected NoSuchElementException"); + } catch (NoSuchElementException expected) { + // Expected + } + } + + @Test + public void testMatchesIterableDirectly() { + Parser parser = of('x'); + String input = "x_x"; + MatchesIterable iterable = + new MatchesIterable<>(parser, input, 0, false); + + assertEquals(parser, iterable.getParser()); + assertEquals(input, iterable.getInput()); + assertEquals(0, iterable.getStart()); + assertFalse(iterable.isOverlapping()); + assertTrue(iterable.toString().contains("MatchesIterable")); + + // Iterating twice produces independent iterators + List firstList = new ArrayList<>(); + iterable.forEach(firstList::add); + assertEquals(Arrays.asList('x', 'x'), firstList); + + List secondList = new ArrayList<>(); + iterable.forEach(secondList::add); + assertEquals(Arrays.asList('x', 'x'), secondList); + + List streamList = iterable.stream().collect(Collectors.toList()); + assertEquals(Arrays.asList('x', 'x'), streamList); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testNegativeStartThrowsInSpliterator() { + new MatchesSpliterator<>(of('a'), "abc", -1, false); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testNegativeStartThrowsInIterator() { + new MatchesIterator<>(of('a'), "abc", -1, false); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testNegativeStartThrowsInIterable() { + new MatchesIterable<>(of('a'), "abc", -1, false); + } + + @Test(expected = NullPointerException.class) + public void testNullParserThrowsInSpliterator() { + new MatchesSpliterator<>(null, "abc", 0, false); + } + + @Test(expected = NullPointerException.class) + public void testNullInputThrowsInSpliterator() { + new MatchesSpliterator<>(of('a'), null, 0, false); + } + + @Test(expected = NullPointerException.class) + public void testNullActionThrowsInSpliteratorAdvance() { + MatchesSpliterator spliterator = + new MatchesSpliterator<>(of('a'), "abc", 0, false); + spliterator.tryAdvance(null); + } + + @Test(expected = NullPointerException.class) + public void testNullActionThrowsInSpliteratorForEachRemaining() { + MatchesSpliterator spliterator = + new MatchesSpliterator<>(of('a'), "abc", 0, false); + spliterator.forEachRemaining(null); + } + + @Test + public void testSideEffectsExecutedExactlyOnce() { + AtomicInteger sideEffectCounter = new AtomicInteger(0); + Parser parser = digit().mapWithSideEffects(d -> { + sideEffectCounter.incrementAndGet(); + return d; + }); + + List list = parser.matchesAsStream("a1b2c3") + .collect(Collectors.toList()); + assertEquals(Arrays.asList('1', '2', '3'), list); + assertEquals(3, sideEffectCounter.get()); + + sideEffectCounter.set(0); + List iterList = new ArrayList<>(); + parser.matches("a1b2c3", 0).forEach(iterList::add); + assertEquals(Arrays.asList('1', '2', '3'), iterList); + assertEquals(3, sideEffectCounter.get()); + } + + @Test + public void testMatchesIteratorLazyConstruction() { + AtomicInteger executionCount = new AtomicInteger(0); + Parser parser = digit().mapWithSideEffects(d -> { + executionCount.incrementAndGet(); + return d; + }); + + MatchesIterator iterator = new MatchesIterator<>(parser, "123", 0, false); + // Construction must be lazy; no parsing or side-effects on creation + assertEquals(0, executionCount.get()); + + assertTrue(iterator.hasNext()); + assertEquals(1, executionCount.get()); + + assertEquals(Character.valueOf('1'), iterator.next()); + assertEquals(1, executionCount.get()); + + assertTrue(iterator.hasNext()); + assertEquals(2, executionCount.get()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/ParsersTest.java b/petitparser-core/src/test/java/org/petitparser/ParsersTest.java index b7a0b9e..5e1f9d0 100644 --- a/petitparser-core/src/test/java/org/petitparser/ParsersTest.java +++ b/petitparser-core/src/test/java/org/petitparser/ParsersTest.java @@ -683,6 +683,8 @@ public void testContinuationSuccessful() { .callCC((continuation, context) -> context.success("Always succeed")); assertTrue(parser.parse("1").isSuccess()); assertTrue(parser.parse("a").isSuccess()); + assertTrue(parser.accept("1")); + assertTrue(parser.accept("a")); } @Test @@ -691,5 +693,33 @@ public void testContinuationFailing() { .callCC((continuation, context) -> context.failure("Always fail")); assertFalse(parser.parse("1").isSuccess()); assertFalse(parser.parse("a").isSuccess()); + assertFalse(parser.accept("1")); + assertFalse(parser.accept("a")); + } + + @Test + public void testDefaultFastParseOn() { + Parser parser = new Parser() { + @Override + public Result parseOn(Context context) { + if (context.getPosition() < context.getBuffer().length() + && context.getBuffer().charAt(context.getPosition()) == 'a') { + return context.success('a', context.getPosition() + 1); + } + return context.failure("a expected"); + } + + @Override + public Parser copy() { + return this; + } + }; + + assertEquals(1, parser.fastParseOn("a", 0)); + assertEquals(2, parser.fastParseOn("ba", 1)); + assertEquals(-1, parser.fastParseOn("b", 0)); + assertEquals(-1, parser.fastParseOn("", 0)); + assertTrue(parser.accept("a")); + assertFalse(parser.accept("b")); } } diff --git a/petitparser-core/src/test/java/org/petitparser/RawTypeCompatibilityTest.java b/petitparser-core/src/test/java/org/petitparser/RawTypeCompatibilityTest.java new file mode 100644 index 0000000..a35f68f --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/RawTypeCompatibilityTest.java @@ -0,0 +1,158 @@ +package org.petitparser; + +import org.junit.Test; +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.StringParser; +import org.petitparser.tools.ExpressionBuilder; +import org.petitparser.tools.GrammarDefinition; +import org.petitparser.tools.GrammarParser; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Verifies raw-type compilation and runtime compatibility for legacy codebases. + */ +@SuppressWarnings({"rawtypes"}) +public class RawTypeCompatibilityTest { + + @Test + public void testRawParserCombinators() { + Parser a = CharacterParser.of('a'); + Parser b = CharacterParser.of('b'); + + Parser seq = a.seq(b); + Parser or = a.or(b); + Parser star = a.star(); + Parser plus = a.plus(); + Parser optional = a.optional(); + Parser not = a.not(); + Parser and = a.and(); + Parser trim = a.trim(); + Parser flatten = a.flatten(); + Parser token = a.token(); + Parser separatedBy = a.separatedBy(b); + Parser delimitedBy = a.delimitedBy(b); + Parser skip = a.skip(b, b); + Parser labeled = a.labeled("label"); + Parser cast = a.cast(); + Parser pick = a.seq(b).pick(0); + Parser permute = a.seq(b).permute(1, 0); + + Parser repeat = a.repeat(1, 3); + Parser starSeparated = a.starSeparated(b); + Parser plusSeparated = a.plusSeparated(b); + Parser times = a.times(2); + Parser negate = a.neg("not a"); + Parser end = a.end(); + Parser mapped = a.map(val -> "val=" + val); + Parser sideEffects = a.mapWithSideEffects(val -> val); + Parser where = a.where(val -> true); + Parser callCC = a.callCC((cont, ctx) -> cont.apply(ctx)); + Parser orWiden = a.orWiden(StringParser.of("hello")); + + assertNotNull(seq); + assertNotNull(or); + assertNotNull(star); + assertNotNull(plus); + assertNotNull(optional); + assertNotNull(not); + assertNotNull(and); + assertNotNull(trim); + assertNotNull(flatten); + assertNotNull(token); + assertNotNull(separatedBy); + assertNotNull(delimitedBy); + assertNotNull(skip); + assertNotNull(labeled); + assertNotNull(cast); + assertNotNull(pick); + assertNotNull(permute); + assertNotNull(repeat); + assertNotNull(starSeparated); + assertNotNull(plusSeparated); + assertNotNull(times); + assertNotNull(negate); + assertNotNull(end); + assertNotNull(mapped); + assertNotNull(sideEffects); + assertNotNull(where); + assertNotNull(callCC); + assertNotNull(orWiden); + } + + @Test + public void testRawSettableParser() { + org.petitparser.parser.combinators.SettableParser settable = + org.petitparser.parser.combinators.SettableParser.undefined(); + Parser parser = settable.plus(); + settable.set(CharacterParser.digit()); + + Result result = parser.parse("123"); + assertTrue(result.isSuccess()); + assertEquals(3, ((List) result.get()).size()); + } + + @Test + public void testRawMatchingMethods() { + Parser parser = CharacterParser.digit().plus().flatten(); + List matches = parser.matches("a123b456c"); + assertEquals(6, matches.size()); + + List skippingMatches = parser.matchesSkipping("a123b456c"); + assertEquals(2, skippingMatches.size()); + assertEquals("123", skippingMatches.get(0)); + assertEquals("456", skippingMatches.get(1)); + } + + @Test + public void testRawParseAndResult() { + Parser parser = CharacterParser.digit().plus().flatten(); + Context context = new Context("123", 0); + Result result = parser.parseOn(context); + + assertTrue(result.isSuccess()); + assertEquals("123", result.get()); + assertEquals(3, result.getPosition()); + + Result quickResult = parser.parse("456"); + assertTrue(quickResult.isSuccess()); + assertEquals("456", quickResult.get()); + } + + @Test + public void testRawGrammarDefinitionAndParser() { + GrammarDefinition definition = new GrammarDefinition() { + { + def("start", ref("number")); + def("number", CharacterParser.digit().plus().flatten()); + } + }; + + GrammarParser parser = new GrammarParser(definition); + Result result = parser.parse("789"); + assertTrue(result.isSuccess()); + assertEquals("789", result.get()); + } + + @Test + public void testRawExpressionBuilder() { + ExpressionBuilder builder = new ExpressionBuilder(); + builder.group() + .primitive(CharacterParser.digit().plus().flatten() + .map(s -> Integer.parseInt((String) s))); + builder.group() + .left(CharacterParser.of('+').trim(), (List values) -> (Integer) values.get(0) + (Integer) values.get(2)); + + Parser parser = builder.build(); + Result result = parser.parse("1 + 2 + 3"); + assertTrue(result.isSuccess()); + assertEquals(Integer.valueOf(6), result.get()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/RegressionsTest.java b/petitparser-core/src/test/java/org/petitparser/RegressionsTest.java index 12a7719..12e770a 100644 --- a/petitparser-core/src/test/java/org/petitparser/RegressionsTest.java +++ b/petitparser-core/src/test/java/org/petitparser/RegressionsTest.java @@ -1,8 +1,13 @@ package org.petitparser; import org.junit.Test; +import org.petitparser.context.Token; import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.StringParser; +import static org.petitparser.Assertions.assertAccept; +import static org.petitparser.Assertions.assertFailure; import static org.petitparser.Assertions.assertSuccess; import static org.petitparser.parser.primitive.CharacterParser.of; @@ -26,4 +31,35 @@ public void testTrimFlatten() { assertSuccess(parser, " a ", " a "); assertSuccess(parser, " a ", " a "); } + + // https://github.com/petitparser/java-petitparser/issues/80 + Parser createLine(String command) { + return StringParser.of(command + " ") + .seq(CharacterParser.word().plusLazy(Token.NEWLINE_PARSER)) + .seq(Token.NEWLINE_PARSER); + } + + @Test + public void testGithubIssue80_before() { + Parser parser = createLine("line1") + .seq(createLine("line2").optional()) + .seq(createLine("line3")) + .end(); + assertAccept(parser, "line1 value\nline3 value\n"); + assertAccept(parser, "line1 value\nline2 value\nline3 value\n"); + assertFailure(parser, "line1 value\nline2 !\nline3 value\n", 12, "line3 expected"); + } + + @Test + public void testGithubIssue80_after() { + Parser parser = createLine("line1") + .seq( + createLine("line3") + .or(createLine("line2") + .seq(createLine("line3")))) + .end(); + assertAccept(parser, "line1 value\nline3 value\n"); + assertAccept(parser, "line1 value\nline2 value\nline3 value\n"); + assertFailure(parser, "line1 value\nline2 !\nline3 value\n", 18, "letter or digit expected"); + } } diff --git a/petitparser-core/src/test/java/org/petitparser/TokenTest.java b/petitparser-core/src/test/java/org/petitparser/TokenTest.java index 8118b09..c7995fd 100644 --- a/petitparser-core/src/test/java/org/petitparser/TokenTest.java +++ b/petitparser-core/src/test/java/org/petitparser/TokenTest.java @@ -4,6 +4,8 @@ import org.petitparser.context.Token; import org.petitparser.parser.Parser; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -11,6 +13,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; import static org.petitparser.parser.primitive.CharacterParser.any; /** @@ -133,4 +136,110 @@ public void testEquals() { } } + @Test + public void testPositionString() { + String text = "a\nb"; + assertEquals("1:1", Token.positionString(text, 0)); + assertEquals("1:2", Token.positionString(text, 1)); + assertEquals("2:1", Token.positionString(text, 2)); + assertEquals("2:2", Token.positionString(text, 3)); + } + + @Test + public void testJoin() { + Token t1 = new Token("abc", 0, 1, 'a'); + Token t2 = new Token("abc", 1, 2, 'b'); + Token joined = Token.join(Arrays.asList(t1, t2)); + assertEquals("abc", joined.getBuffer()); + assertEquals(0, joined.getStart()); + assertEquals(2, joined.getStop()); + assertEquals("ab", joined.getInput()); + assertEquals(Arrays.asList('a', 'b'), joined.getValue()); + + Token single = Token.join(Collections.singletonList(t1)); + assertEquals("abc", single.getBuffer()); + assertEquals(0, single.getStart()); + assertEquals(1, single.getStop()); + assertEquals(Collections.singletonList('a'), single.getValue()); + + Token varargs = Token.join(t1, t2); + assertEquals(joined, varargs); + + Token customValue = Token.join(Arrays.asList(t1, t2), "ab"); + assertEquals("abc", customValue.getBuffer()); + assertEquals(0, customValue.getStart()); + assertEquals(2, customValue.getStop()); + assertEquals("ab", customValue.getValue()); + } + + @Test + public void testJoinErrors() { + try { + Token.join(Collections.emptyList()); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + Token.join(Collections.emptyList(), "value"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + Token.join((Iterable) null); + fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + + try { + Token.join((Token[]) null); + fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + + try { + Token t1 = new Token("abc", 0, 1, 'a'); + Token.join(Arrays.asList(t1, null)); + fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + + try { + Token t1 = new Token("abc", 0, 1, 'a'); + Token t2 = new Token("xyz", 0, 1, 'x'); + Token.join(Arrays.asList(t1, t2)); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + Token t1 = new Token("abc", 0, 1, 'a'); + Token t2 = new Token("xyz", 0, 1, 'x'); + Token.join(Arrays.asList(t1, t2), "value"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testToPositionString() { + Token token = new Token("foo\nbar", 4, 7, "bar"); + assertEquals("2:1", token.toPositionString()); + assertEquals(Token.positionString("foo\nbar", 4), token.toPositionString()); + } + + @Test + public void testTokenParserFastParse() { + Parser tokenParser = any().token(); + assertEquals(1, tokenParser.fastParseOn("a", 0)); + assertEquals(-1, tokenParser.fastParseOn("", 0)); + } } diff --git a/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkResult.java b/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkResult.java new file mode 100644 index 0000000..c554823 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkResult.java @@ -0,0 +1,55 @@ +package org.petitparser.benchmark; + +import java.util.Objects; + +/** + * Encapsulates the execution measurements of a benchmark run. + */ +public class BenchmarkResult { + + private final String name; + private final int iterations; + private final long elapsedNanos; + private final long allocatedBytes; + + public BenchmarkResult(String name, int iterations, long elapsedNanos, long allocatedBytes) { + this.name = Objects.requireNonNull(name, "name must not be null"); + this.iterations = iterations; + this.elapsedNanos = elapsedNanos; + this.allocatedBytes = Math.max(0, allocatedBytes); + } + + public String getName() { + return name; + } + + public int getIterations() { + return iterations; + } + + public long getElapsedNanos() { + return elapsedNanos; + } + + public long getAllocatedBytes() { + return allocatedBytes; + } + + public double getOperationsPerSecond() { + return elapsedNanos > 0 ? (iterations * 1_000_000_000.0) / elapsedNanos : 0.0; + } + + public double getNanosPerOperation() { + return iterations > 0 ? (double) elapsedNanos / iterations : 0.0; + } + + public double getBytesPerOperation() { + return iterations > 0 ? (double) allocatedBytes / iterations : 0.0; + } + + @Override + public String toString() { + return String.format("%-42s %12.2f ops/s %9.2f ns/op %9.2f B/op", + name, getOperationsPerSecond(), getNanosPerOperation(), getBytesPerOperation()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkRunner.java b/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkRunner.java new file mode 100644 index 0000000..e2ec3ae --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkRunner.java @@ -0,0 +1,87 @@ +package org.petitparser.benchmark; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * Runner harness for benchmarks tracking elapsed time and heap allocations. + */ +public class BenchmarkRunner { + + private BenchmarkRunner() {} + + private static final MethodHandle GET_THREAD_ALLOCATED_BYTES_MH; + + static { + Object bean = null; + MethodHandle mh = null; + try { + Class factoryClass = Class.forName("java.lang.management.ManagementFactory"); + Method getMxBean = factoryClass.getMethod("getThreadMXBean"); + bean = getMxBean.invoke(null); + if (bean != null) { + Class sunBeanClass = Class.forName("com.sun.management.ThreadMXBean"); + if (sunBeanClass.isInstance(bean)) { + Method isSupported = sunBeanClass.getMethod("isThreadAllocatedMemorySupported"); + Boolean supported = (Boolean) isSupported.invoke(bean); + if (Boolean.TRUE.equals(supported)) { + Method isEnabled = sunBeanClass.getMethod("isThreadAllocatedMemoryEnabled"); + Boolean enabled = (Boolean) isEnabled.invoke(bean); + if (!Boolean.TRUE.equals(enabled)) { + Method setEnabled = sunBeanClass.getMethod("setThreadAllocatedMemoryEnabled", boolean.class); + setEnabled.invoke(bean, true); + } + Method getBytes = sunBeanClass.getMethod("getThreadAllocatedBytes", long.class); + mh = java.lang.invoke.MethodHandles.lookup().unreflect(getBytes).bindTo(bean); + } + } + } + } catch (Throwable ignored) { + bean = null; + mh = null; + } + GET_THREAD_ALLOCATED_BYTES_MH = mh; + } + + public static boolean isThreadAllocationTrackingSupported() { + return GET_THREAD_ALLOCATED_BYTES_MH != null; + } + + @SuppressWarnings("deprecation") + public static long getThreadAllocatedBytes() { + if (GET_THREAD_ALLOCATED_BYTES_MH != null) { + try { + return (long) GET_THREAD_ALLOCATED_BYTES_MH.invokeExact(Thread.currentThread().getId()); + } catch (Throwable ignored) { + } + } + Runtime runtime = Runtime.getRuntime(); + return runtime.totalMemory() - runtime.freeMemory(); + } + + public static BenchmarkResult run(String name, int warmupIterations, int measurementIterations, Runnable task) { + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(task, "task must not be null"); + + // Warmup phase + for (int i = 0; i < warmupIterations; i++) { + task.run(); + } + + // Measurement phase + System.gc(); + long startAllocated = getThreadAllocatedBytes(); + long startNanos = System.nanoTime(); + + for (int i = 0; i < measurementIterations; i++) { + task.run(); + } + + long elapsedNanos = System.nanoTime() - startNanos; + long endAllocated = getThreadAllocatedBytes(); + long allocated = endAllocated >= startAllocated ? endAllocated - startAllocated : 0; + + return new BenchmarkResult(name, measurementIterations, elapsedNanos, allocated); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkSuite.java b/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkSuite.java new file mode 100644 index 0000000..6ae1097 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkSuite.java @@ -0,0 +1,137 @@ +package org.petitparser.benchmark; + +import org.petitparser.context.Context; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.CharacterPredicate; +import org.petitparser.parser.primitive.LookupCharPredicate; +import org.petitparser.parser.primitive.RangesCharPredicate; + +import java.util.ArrayList; +import java.util.List; + +/** + * Performance and allocation benchmark suite verifying PetitParser Java optimizations. + */ +public class BenchmarkSuite { + + private BenchmarkSuite() {} + + private static final String REPEATING_INPUT = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".repeat(20); + + private static final String PREDICATE_INPUT = + "aB3_!@#xY9$%-+zK7*&^~12345abcdefghijKLMNOPQRST".repeat(30); + + public static List runAll(int warmup, int iterations) { + List results = new ArrayList<>(); + results.addAll(benchmarkRepeatingCharacterVsStarFlatten(warmup, iterations)); + results.addAll(benchmarkLookupTableVsRanges(warmup, iterations)); + results.addAll(benchmarkFastParseZeroAllocation(warmup, iterations)); + return results; + } + + public static List benchmarkRepeatingCharacterVsStarFlatten(int warmup, int iterations) { + List results = new ArrayList<>(); + + // 1. RepeatingCharacterParser (Zero-List Lexing) + Parser repeatingParser = CharacterParser.word().plusString(); + BenchmarkResult r1 = BenchmarkRunner.run("RepeatingCharacterParser.parseOn", warmup, iterations, () -> { + repeatingParser.parseOn(new Context(REPEATING_INPUT, 0)); + }); + results.add(r1); + + // 2. PossessiveRepeatingParser + FlattenParser (Legacy List Allocation) + Parser flattenParser = CharacterParser.word().plus().flatten(); + BenchmarkResult r2 = BenchmarkRunner.run("Word.plus().flatten().parseOn", warmup, iterations, () -> { + flattenParser.parseOn(new Context(REPEATING_INPUT, 0)); + }); + results.add(r2); + + // 3. RepeatingCharacterParser fastParseOn + BenchmarkResult r3 = BenchmarkRunner.run("RepeatingCharacterParser.fastParseOn", warmup, iterations, () -> { + repeatingParser.fastParseOn(REPEATING_INPUT, 0); + }); + results.add(r3); + + // 4. FlattenParser fastParseOn + BenchmarkResult r4 = BenchmarkRunner.run("Word.plus().flatten().fastParseOn", warmup, iterations, () -> { + flattenParser.fastParseOn(REPEATING_INPUT, 0); + }); + results.add(r4); + + return results; + } + + public static List benchmarkLookupTableVsRanges(int warmup, int iterations) { + List results = new ArrayList<>(); + + // Word predicate via O(1) Lookup table + boolean[] table = new boolean[256]; + for (char c = 'a'; c <= 'z'; c++) table[c] = true; + for (char c = 'A'; c <= 'Z'; c++) table[c] = true; + for (char c = '0'; c <= '9'; c++) table[c] = true; + table['_'] = true; + CharacterPredicate lookupPredicate = new LookupCharPredicate(table); + + // Word predicate via binary search ranges + char[] starts = new char[]{'0', 'A', '_', 'a'}; + char[] stops = new char[]{'9', 'Z', '_', 'z'}; + CharacterPredicate rangesPredicate = new RangesCharPredicate(starts, stops); + + BenchmarkResult r1 = BenchmarkRunner.run("LookupCharPredicate (O(1) table)", warmup, iterations, () -> { + for (int i = 0; i < PREDICATE_INPUT.length(); i++) { + lookupPredicate.test(PREDICATE_INPUT.charAt(i)); + } + }); + results.add(r1); + + BenchmarkResult r2 = BenchmarkRunner.run("RangesCharPredicate (binary search)", warmup, iterations, () -> { + for (int i = 0; i < PREDICATE_INPUT.length(); i++) { + rangesPredicate.test(PREDICATE_INPUT.charAt(i)); + } + }); + results.add(r2); + + return results; + } + + public static List benchmarkFastParseZeroAllocation(int warmup, int iterations) { + List results = new ArrayList<>(); + + // Grammar: word + spaces + '=' + spaces + word + Parser assignment = CharacterParser.word().plusString() + .seq(CharacterParser.whitespace().star()) + .seq(CharacterParser.of('=')) + .seq(CharacterParser.whitespace().star()) + .seq(CharacterParser.word().plusString()); + + String input = "myVariable = value123"; + + BenchmarkResult rFast = BenchmarkRunner.run("fastParseOn (Zero-Allocation)", warmup, iterations, () -> { + assignment.fastParseOn(input, 0); + }); + results.add(rFast); + + BenchmarkResult rFull = BenchmarkRunner.run("parseOn (Context/Result Allocation)", warmup, iterations, () -> { + assignment.parseOn(new Context(input, 0)); + }); + results.add(rFull); + + return results; + } + + public static void main(String[] args) { + System.out.println("================================================================================"); + System.out.println("PetitParser Java Benchmark Suite"); + System.out.println("================================================================================"); + int warmup = 15000; + int iterations = 25000; + + List results = runAll(warmup, iterations); + for (BenchmarkResult result : results) { + System.out.println(result); + } + System.out.println("================================================================================"); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkSuiteTest.java b/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkSuiteTest.java new file mode 100644 index 0000000..206146b --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/benchmark/BenchmarkSuiteTest.java @@ -0,0 +1,208 @@ +package org.petitparser.benchmark; + +import org.junit.Test; +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.CharacterPredicate; +import org.petitparser.parser.primitive.LookupCharPredicate; +import org.petitparser.parser.primitive.RangesCharPredicate; +import org.petitparser.parser.primitive.StringParser; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Parallel test suite verifying performance, throughput, and zero-allocation characteristics. + */ +public class BenchmarkSuiteTest { + + @Test + public void testRepeatingCharacterAllocationAndFunctionalParity() { + String input = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".repeat(10); + Parser repeatingParser = CharacterParser.word().plusString(); + Parser flattenParser = CharacterParser.word().plus().flatten(); + + // Functional parity verification + Result repeatingResult = repeatingParser.parseOn(new Context(input, 0)); + Result flattenResult = flattenParser.parseOn(new Context(input, 0)); + + assertTrue(repeatingResult.isSuccess()); + assertTrue(flattenResult.isSuccess()); + assertEquals(input, repeatingResult.get()); + assertEquals(input, flattenResult.get()); + assertEquals(input.length(), repeatingResult.getPosition()); + assertEquals(input.length(), flattenResult.getPosition()); + + assertEquals(input.length(), repeatingParser.fastParseOn(input, 0)); + assertEquals(input.length(), flattenParser.fastParseOn(input, 0)); + + // Allocation comparison + List results = BenchmarkSuite.benchmarkRepeatingCharacterVsStarFlatten(2000, 5000); + assertEquals(4, results.size()); + + BenchmarkResult rRepeating = results.get(0); + BenchmarkResult rFlatten = results.get(1); + BenchmarkResult rFastRepeating = results.get(2); + + if (BenchmarkRunner.isThreadAllocationTrackingSupported()) { + // RepeatingCharacterParser should allocate far fewer bytes than .star().flatten() + assertTrue("Repeating parser (" + rRepeating.getAllocatedBytes() + "B) should allocate less than flatten (" + + rFlatten.getAllocatedBytes() + "B)", + rRepeating.getAllocatedBytes() < rFlatten.getAllocatedBytes()); + + // fastParseOn on repeating parser should have zero heap allocations + assertEquals("fastParseOn should allocate 0 bytes", 0L, rFastRepeating.getAllocatedBytes()); + } + } + + @Test + public void testLookupTableVsRangesParity() { + boolean[] table = new boolean[256]; + for (char c = 'a'; c <= 'z'; c++) table[c] = true; + for (char c = 'A'; c <= 'Z'; c++) table[c] = true; + for (char c = '0'; c <= '9'; c++) table[c] = true; + table['_'] = true; + CharacterPredicate lookup = new LookupCharPredicate(table); + + char[] starts = new char[]{'0', 'A', '_', 'a'}; + char[] stops = new char[]{'9', 'Z', '_', 'z'}; + CharacterPredicate ranges = new RangesCharPredicate(starts, stops); + + // Verify functional parity across all 65536 char values + for (int c = 0; c <= Character.MAX_VALUE; c++) { + char ch = (char) c; + assertEquals("Discrepancy at char " + c, lookup.test(ch), ranges.test(ch)); + } + + List results = BenchmarkSuite.benchmarkLookupTableVsRanges(500, 2000); + assertEquals(2, results.size()); + assertTrue(results.get(0).getOperationsPerSecond() > 0); + assertTrue(results.get(1).getOperationsPerSecond() > 0); + } + + @Test + public void testFastParseZeroAllocationAcrossParsers() { + if (!BenchmarkRunner.isThreadAllocationTrackingSupported()) { + return; + } + + String input = "test123Identifier = 42;"; + + Parser parser = CharacterParser.word().plusString() + .seq(CharacterParser.whitespace().star()) + .seq(CharacterParser.of('=')) + .seq(CharacterParser.whitespace().star()) + .seq(CharacterParser.digit().plusString()) + .seq(CharacterParser.of(';')); + + Parser stringParser = StringParser.of("test123Identifier = 42;"); + + // Warmup + for (int i = 0; i < 2000; i++) { + parser.fastParseOn(input, 0); + parser.fastParseOn(input, 5); // failure path + stringParser.fastParseOn(input, 0); + stringParser.fastParseOn(input, 5); // failure path + } + + // Measure success path + long start = BenchmarkRunner.getThreadAllocatedBytes(); + for (int i = 0; i < 5000; i++) { + int pos = parser.fastParseOn(input, 0); + if (pos < 0) { + throw new IllegalStateException(); + } + } + long allocatedSuccess = BenchmarkRunner.getThreadAllocatedBytes() - start; + assertEquals("fastParseOn success should allocate 0 bytes", 0L, allocatedSuccess); + + // Measure failure path + start = BenchmarkRunner.getThreadAllocatedBytes(); + for (int i = 0; i < 5000; i++) { + int pos = parser.fastParseOn(input, 1); + if (pos >= 0) { + throw new IllegalStateException(); + } + } + long allocatedFailure = BenchmarkRunner.getThreadAllocatedBytes() - start; + assertEquals("fastParseOn failure should allocate 0 bytes", 0L, allocatedFailure); + + // StringParser fastParseOn + start = BenchmarkRunner.getThreadAllocatedBytes(); + for (int i = 0; i < 5000; i++) { + stringParser.fastParseOn(input, 0); + stringParser.fastParseOn(input, 1); + } + long allocatedString = BenchmarkRunner.getThreadAllocatedBytes() - start; + assertEquals("StringParser fastParseOn should allocate 0 bytes", 0L, allocatedString); + } + + @Test + public void testBenchmarkResultMethods() { + BenchmarkResult result = new BenchmarkResult("TestBenchmark", 1000, 1_000_000_000L, 5000); + assertEquals("TestBenchmark", result.getName()); + assertEquals(1000, result.getIterations()); + assertEquals(1_000_000_000L, result.getElapsedNanos()); + assertEquals(5000L, result.getAllocatedBytes()); + + assertEquals(1000.0, result.getOperationsPerSecond(), 0.001); + assertEquals(1_000_000.0, result.getNanosPerOperation(), 0.001); + assertEquals(5.0, result.getBytesPerOperation(), 0.001); + + String str = result.toString(); + assertTrue(str.contains("TestBenchmark")); + assertTrue(str.contains("ops/s")); + assertTrue(str.contains("B/op")); + } + + @Test + public void testBenchmarkResultZeroIterationsOrElapsed() { + BenchmarkResult zeroElapsed = new BenchmarkResult("ZeroElapsed", 100, 0, 0); + assertEquals(0.0, zeroElapsed.getOperationsPerSecond(), 0.001); + + BenchmarkResult zeroIters = new BenchmarkResult("ZeroIters", 0, 100, 0); + assertEquals(0.0, zeroIters.getNanosPerOperation(), 0.001); + assertEquals(0.0, zeroIters.getBytesPerOperation(), 0.001); + } + + @Test + public void testBenchmarkSuiteRunAllAndMain() { + List results = BenchmarkSuite.runAll(100, 500); + assertNotNull(results); + assertFalse(results.isEmpty()); + for (BenchmarkResult result : results) { + assertNotNull(result.getName()); + assertTrue(result.getOperationsPerSecond() > 0); + } + + // Call main with dry-run + BenchmarkSuite.main(new String[0]); + } + + @Test(expected = NullPointerException.class) + public void testBenchmarkResultNullName() { + new BenchmarkResult(null, 10, 10, 10); + } + + @Test + public void testBenchmarkResultNegativeAllocatedBytes() { + BenchmarkResult result = new BenchmarkResult("NegativeAlloc", 10, 100, -50); + assertEquals(0L, result.getAllocatedBytes()); + } + + @Test(expected = NullPointerException.class) + public void testBenchmarkRunnerNullName() { + BenchmarkRunner.run(null, 1, 1, () -> {}); + } + + @Test(expected = NullPointerException.class) + public void testBenchmarkRunnerNullTask() { + BenchmarkRunner.run("Test", 1, 1, null); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/context/ContextTest.java b/petitparser-core/src/test/java/org/petitparser/context/ContextTest.java new file mode 100644 index 0000000..d807f69 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/context/ContextTest.java @@ -0,0 +1,37 @@ +package org.petitparser.context; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link Context}. + */ +public class ContextTest { + + @Test + public void testContextPositionString() { + Context context = new Context("a\nb", 2); + assertEquals("a\nb", context.getBuffer()); + assertEquals(2, context.getPosition()); + assertEquals("2:1", context.toPositionString()); + assertEquals("Context[2:1]", context.toString()); + } + + @Test + public void testSuccessAndFailure() { + Context context = new Context("a\nb", 2); + Success success = context.success("ok"); + assertTrue(success.isSuccess()); + assertEquals("ok", success.get()); + assertEquals(2, success.getPosition()); + assertEquals("Success[2:1]: ok", success.toString()); + + Failure failure = context.failure("error"); + assertTrue(failure.isFailure()); + assertEquals("error", failure.getMessage()); + assertEquals(2, failure.getPosition()); + assertEquals("Failure[2:1]: error", failure.toString()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/MatchesIterableTest.java b/petitparser-core/src/test/java/org/petitparser/parser/MatchesIterableTest.java new file mode 100644 index 0000000..807525a --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/MatchesIterableTest.java @@ -0,0 +1,73 @@ +package org.petitparser.parser; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.of; + +/** + * Tests {@link MatchesIterable}. + */ +public class MatchesIterableTest { + + @Test + public void testPropertiesAndMultipleIterations() { + Parser parser = of('x'); + MatchesIterable iterable = + new MatchesIterable<>(parser, "x_x", 0, false); + + assertEquals(parser, iterable.getParser()); + assertEquals("x_x", iterable.getInput()); + assertEquals(0, iterable.getStart()); + assertFalse(iterable.isOverlapping()); + assertTrue(iterable.toString().contains("MatchesIterable")); + + List first = new ArrayList<>(); + iterable.forEach(first::add); + assertEquals(Arrays.asList('x', 'x'), first); + + List second = new ArrayList<>(); + iterable.forEach(second::add); + assertEquals(Arrays.asList('x', 'x'), second); + + assertNotNull(iterable.iterator()); + assertNotNull(iterable.spliterator()); + assertEquals(Arrays.asList('x', 'x'), iterable.stream().collect(Collectors.toList())); + } + + @Test + public void testOverlappingIterable() { + Parser parser = digit().seq(digit()).flatten(); + MatchesIterable iterable = + new MatchesIterable<>(parser, "123", 0, true); + + assertTrue(iterable.isOverlapping()); + List list = new ArrayList<>(); + iterable.forEach(list::add); + assertEquals(Arrays.asList("12", "23"), list); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testNegativeStartThrows() { + new MatchesIterable<>(of('a'), "abc", -1, false); + } + + @Test(expected = NullPointerException.class) + public void testNullParserThrows() { + new MatchesIterable<>(null, "abc", 0, false); + } + + @Test(expected = NullPointerException.class) + public void testNullInputThrows() { + new MatchesIterable<>(of('a'), null, 0, false); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/MatchesIteratorTest.java b/petitparser-core/src/test/java/org/petitparser/parser/MatchesIteratorTest.java new file mode 100644 index 0000000..6dc1b2d --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/MatchesIteratorTest.java @@ -0,0 +1,106 @@ +package org.petitparser.parser; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.of; + +/** + * Tests {@link MatchesIterator}. + */ +public class MatchesIteratorTest { + + @Test + public void testPropertiesAndIteration() { + Parser parser = of('x'); + MatchesIterator iterator = + new MatchesIterator<>(parser, "x_x", 0, false); + + assertEquals(parser, iterator.getParser()); + assertEquals("x_x", iterator.getInput()); + assertEquals(0, iterator.getStart()); + assertEquals(0, iterator.getCurrent()); + assertFalse(iterator.isOverlapping()); + assertTrue(iterator.toString().contains("MatchesIterator")); + + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + assertEquals(Arrays.asList('x', 'x'), list); + assertFalse(iterator.hasNext()); + assertTrue(iterator.getCurrent() > 0); + } + + @Test(expected = NoSuchElementException.class) + public void testNextBeyondExhaustionThrows() { + MatchesIterator iterator = + new MatchesIterator<>(of('x'), "x", 0, false); + assertTrue(iterator.hasNext()); + assertEquals(Character.valueOf('x'), iterator.next()); + assertFalse(iterator.hasNext()); + iterator.next(); + } + + @Test + public void testOverlappingIteration() { + Parser parser = digit().seq(digit()).flatten(); + MatchesIterator iterator = + new MatchesIterator<>(parser, "123", 0, true); + + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + assertEquals(Arrays.asList("12", "23"), list); + } + + @Test + public void testNonOverlappingIteration() { + Parser parser = digit().seq(digit()).flatten(); + MatchesIterator iterator = + new MatchesIterator<>(parser, "1234", 0, false); + + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + assertEquals(Arrays.asList("12", "34"), list); + } + + @Test + public void testZeroLengthMatchProgresses() { + Parser parser = digit().star().flatten(); + MatchesIterator iterator = + new MatchesIterator<>(parser, "a12b", 0, false); + + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + assertEquals(Arrays.asList("", "12", "", ""), list); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testNegativeStartThrows() { + new MatchesIterator<>(of('a'), "abc", -1, false); + } + + @Test(expected = NullPointerException.class) + public void testNullParserThrows() { + new MatchesIterator<>(null, "abc", 0, false); + } + + @Test(expected = NullPointerException.class) + public void testNullInputThrows() { + new MatchesIterator<>(of('a'), null, 0, false); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/MatchesSpliteratorTest.java b/petitparser-core/src/test/java/org/petitparser/parser/MatchesSpliteratorTest.java new file mode 100644 index 0000000..a25c02e --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/MatchesSpliteratorTest.java @@ -0,0 +1,120 @@ +package org.petitparser.parser; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Spliterator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.of; + +/** + * Tests {@link MatchesSpliterator}. + */ +public class MatchesSpliteratorTest { + + @Test + public void testCharacteristicsAndProperties() { + Parser parser = digit(); + MatchesSpliterator spliterator = + new MatchesSpliterator<>(parser, "123", 0, true); + + assertEquals(parser, spliterator.getParser()); + assertEquals("123", spliterator.getInput()); + assertEquals(0, spliterator.getStart()); + assertEquals(0, spliterator.getCurrent()); + assertTrue(spliterator.isOverlapping()); + assertNull(spliterator.trySplit()); + assertEquals(Long.MAX_VALUE, spliterator.estimateSize()); + assertEquals(Spliterator.ORDERED, spliterator.characteristics()); + assertTrue(spliterator.toString().contains("MatchesSpliterator")); + } + + @Test + public void testOverlappingAdvance() { + Parser parser = digit().seq(digit()).flatten(); + MatchesSpliterator spliterator = + new MatchesSpliterator<>(parser, "1234", 0, true); + + List results = new ArrayList<>(); + while (spliterator.tryAdvance(results::add)) { + // Loop until false + } + assertEquals(Arrays.asList("12", "23", "34"), results); + assertFalse(spliterator.tryAdvance(results::add)); + } + + @Test + public void testNonOverlappingTryAdvance() { + Parser parser = digit().seq(digit()).flatten(); + MatchesSpliterator spliterator = + new MatchesSpliterator<>(parser, "1234", 0, false); + + List results = new ArrayList<>(); + assertEquals(0, spliterator.getCurrent()); + assertTrue(spliterator.tryAdvance(results::add)); + assertEquals(2, spliterator.getCurrent()); + assertTrue(spliterator.tryAdvance(results::add)); + assertEquals(4, spliterator.getCurrent()); + assertEquals(Arrays.asList("12", "34"), results); + assertFalse(spliterator.tryAdvance(results::add)); + assertEquals(5, spliterator.getCurrent()); + } + + @Test + public void testNonOverlappingForEachRemaining() { + Parser parser = digit().seq(digit()).flatten(); + MatchesSpliterator spliterator = + new MatchesSpliterator<>(parser, "1234", 0, false); + + List results = new ArrayList<>(); + spliterator.forEachRemaining(results::add); + assertEquals(Arrays.asList("12", "34"), results); + } + + @Test + public void testZeroLengthMatchProgresses() { + Parser parser = digit().star().flatten(); + MatchesSpliterator spliterator = + new MatchesSpliterator<>(parser, "a12b", 0, false); + + List results = new ArrayList<>(); + spliterator.forEachRemaining(results::add); + assertEquals(Arrays.asList("", "12", "", ""), results); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testNegativeStartThrows() { + new MatchesSpliterator<>(of('a'), "abc", -1, false); + } + + @Test(expected = NullPointerException.class) + public void testNullParserThrows() { + new MatchesSpliterator<>(null, "abc", 0, false); + } + + @Test(expected = NullPointerException.class) + public void testNullInputThrows() { + new MatchesSpliterator<>(of('a'), null, 0, false); + } + + @Test(expected = NullPointerException.class) + public void testNullActionInTryAdvanceThrows() { + MatchesSpliterator spliterator = + new MatchesSpliterator<>(of('a'), "abc", 0, false); + spliterator.tryAdvance(null); + } + + @Test(expected = NullPointerException.class) + public void testNullActionInForEachRemainingThrows() { + MatchesSpliterator spliterator = + new MatchesSpliterator<>(of('a'), "abc", 0, false); + spliterator.forEachRemaining(null); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/actions/CastParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/actions/CastParserTest.java new file mode 100644 index 0000000..c943897 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/actions/CastParserTest.java @@ -0,0 +1,173 @@ +package org.petitparser.parser.actions; + +import org.junit.Test; +import org.petitparser.parser.Parser; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; +import static org.petitparser.parser.primitive.CharacterParser.any; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.letter; + +/** + * Tests for {@link CastParser} and {@link CastListParser}. + */ +public class CastParserTest { + + @Test + public void testCastDefault() { + Parser parser = digit().map((Character c) -> Character.getNumericValue(c)).cast(); + assertSuccess(parser, "1", 1); + assertFailure(parser, "a", 0, "digit expected"); + } + + @Test + public void testCastWithClass() { + Parser parser = digit().map((Character c) -> (Number) Character.getNumericValue(c)).cast(Number.class); + assertSuccess(parser, "1", 1); + assertFailure(parser, "a", 0, "digit expected"); + } + + @Test + public void testCastNull() { + Parser parser = Parser.epsilon(null).cast(String.class); + assertSuccess(parser, "", null); + } + + @Test + public void testCastWithClassMismatchThrows() { + Parser parser = letter().cast(Integer.class); + try { + parser.parse("a"); + fail("Expected ClassCastException"); + } catch (ClassCastException expected) { + // expected + } + } + + @Test + public void testCastFastParse() { + Parser parser = digit().cast(Character.class); + assertEquals(1, parser.fastParseOn("1", 0)); + assertEquals(-1, parser.fastParseOn("a", 0)); + assertEquals(2, parser.fastParseOn("a1", 1)); + } + + @Test + public void testCastGetters() { + CastParser defaultParser = new CastParser<>(any()); + assertNull(defaultParser.getTargetClass()); + + CastParser classParser = new CastParser<>(any(), String.class); + assertSame(String.class, classParser.getTargetClass()); + } + + @Test + public void testCastCopyAndEquality() { + Parser parser = any().cast(String.class); + Parser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + Parser differentClass = any().cast(Integer.class); + assertFalse(parser.isEqualTo(differentClass)); + + Parser unTyped = any().cast(); + assertFalse(parser.isEqualTo(unTyped)); + assertTrue(unTyped.isEqualTo(unTyped.copy())); + + Parser differentDelegate = digit().cast(String.class); + assertFalse(parser.isEqualTo(differentDelegate)); + } + + @Test + public void testCastToString() { + Parser defaultParser = any().cast(); + assertEquals("CastParser", defaultParser.toString()); + + Parser classParser = any().cast(String.class); + assertEquals("CastParser[String]", classParser.toString()); + } + + @Test + public void testCastListDefault() { + Parser parser = digit().times(3).castList(); + assertSuccess(parser, "123", Arrays.asList('1', '2', '3')); + assertFailure(parser, "12a", 2, "digit expected"); + } + + @Test + public void testCastListWithClass() { + Parser parser = digit().times(3).castList(Character.class); + assertSuccess(parser, "123", Arrays.asList('1', '2', '3')); + assertFailure(parser, "12a", 2, "digit expected"); + } + + @Test + public void testCastListMismatchThrows() { + Parser parser = digit().times(2).castList(Integer.class); + try { + parser.parse("12"); + fail("Expected ClassCastException"); + } catch (ClassCastException expected) { + // expected + } + } + + @Test + public void testCastListFastParse() { + Parser parser = digit().times(2).castList(Character.class); + assertEquals(2, parser.fastParseOn("12", 0)); + assertEquals(-1, parser.fastParseOn("1a", 0)); + assertEquals(3, parser.fastParseOn("x12", 1)); + } + + @Test + public void testCastListGetters() { + CastListParser defaultParser = new CastListParser<>(any().star()); + assertNull(defaultParser.getElementClass()); + + CastListParser classParser = new CastListParser<>(any().star(), String.class); + assertSame(String.class, classParser.getElementClass()); + } + + @Test + public void testCastListCopyAndEquality() { + Parser parser = any().star().castList(String.class); + Parser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + Parser differentClass = any().star().castList(Integer.class); + assertFalse(parser.isEqualTo(differentClass)); + + Parser unTyped = any().star().castList(); + assertFalse(parser.isEqualTo(unTyped)); + assertTrue(unTyped.isEqualTo(unTyped.copy())); + + Parser differentDelegate = digit().star().castList(String.class); + assertFalse(parser.isEqualTo(differentDelegate)); + } + + @Test + public void testCastListToString() { + Parser defaultParser = any().star().castList(); + assertEquals("CastListParser", defaultParser.toString()); + + Parser classParser = any().star().castList(String.class); + assertEquals("CastListParser[String]", classParser.toString()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/actions/ContinuationParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/actions/ContinuationParserTest.java new file mode 100644 index 0000000..b79ecef --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/actions/ContinuationParserTest.java @@ -0,0 +1,121 @@ +package org.petitparser.parser.actions; + +import org.junit.Test; +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link ContinuationParser}. + */ +public class ContinuationParserTest { + + @Test(expected = NullPointerException.class) + public void testNullHandler() { + new ContinuationParser(CharacterParser.digit(), null); + } + + @Test + public void testFastParseSuccessfulContinuation() { + Parser parser = CharacterParser.digit() + .callCC((continuation, context) -> context.success("Always succeed")); + + // parseOn succeeds on 'a' even though digit() fails + assertTrue(parser.parse("a").isSuccess()); + assertEquals(0, parser.parse("a").getPosition()); + + // fastParseOn must also respect the handler and succeed + assertEquals(0, parser.fastParseOn("a", 0)); + assertTrue(parser.accept("a")); + } + + @Test + public void testFastParseFailingContinuation() { + Parser parser = CharacterParser.digit() + .callCC((continuation, context) -> context.failure("Always fail")); + + // parseOn fails on '1' even though digit() succeeds + assertFalse(parser.parse("1").isSuccess()); + + // fastParseOn must also respect the handler and fail + assertEquals(-1, parser.fastParseOn("1", 0)); + assertFalse(parser.accept("1")); + } + + @Test + public void testFastParseSideEffectsInContinuation() { + AtomicInteger invocationCount = new AtomicInteger(0); + Parser parser = CharacterParser.digit().callCC((continuation, context) -> { + invocationCount.incrementAndGet(); + return continuation.apply(context); + }); + + assertEquals(1, parser.fastParseOn("1", 0)); + assertEquals(1, invocationCount.get()); + + assertEquals(-1, parser.fastParseOn("a", 0)); + assertEquals(2, invocationCount.get()); + } + + @Test + public void testContinuationRedirecting() { + Parser parser = CharacterParser.digit().callCC( + (continuation, context) -> CharacterParser.letter().parseOn(context)); + + assertFalse(parser.parse("1").isSuccess()); + assertTrue(parser.parse("a").isSuccess()); + + assertEquals(-1, parser.fastParseOn("1", 0)); + assertEquals(1, parser.fastParseOn("a", 0)); + assertFalse(parser.accept("1")); + assertTrue(parser.accept("a")); + } + + @Test + public void testContinuationResuming() { + List> continuations = new ArrayList<>(); + List contexts = new ArrayList<>(); + Parser parser = CharacterParser.digit().callCC((continuation, context) -> { + continuations.add(continuation); + contexts.add(context); + return context.failure("Abort"); + }); + + assertFalse(parser.parse("1").isSuccess()); + assertFalse(parser.parse("a").isSuccess()); + + assertTrue(continuations.get(0).apply(contexts.get(0)).isSuccess()); + assertFalse(continuations.get(1).apply(contexts.get(1)).isSuccess()); + } + + @Test + public void testCopyAndEquality() { + ContinuationParser.ContinuationHandler handler = (c, ctx) -> c.apply(ctx); + ContinuationParser parser1 = new ContinuationParser(CharacterParser.digit(), handler); + ContinuationParser parser2 = new ContinuationParser(CharacterParser.digit(), handler); + ContinuationParser parser3 = new ContinuationParser(CharacterParser.letter(), handler); + + assertTrue(parser1.isEqualTo(parser2)); + assertFalse(parser1.isEqualTo(parser3)); + + ContinuationParser copy = parser1.copy(); + assertNotSame(parser1, copy); + assertTrue(parser1.isEqualTo(copy)); + + Parser child = parser1.getChildren().get(0); + Parser newChild = CharacterParser.letter(); + parser1.replace(child, newChild); + assertEquals(newChild, parser1.getChildren().get(0)); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/actions/PickParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/actions/PickParserTest.java new file mode 100644 index 0000000..8e0af8b --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/actions/PickParserTest.java @@ -0,0 +1,207 @@ +package org.petitparser.parser.actions; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.letter; + +/** + * Tests for {@link PickParser} and {@link PermuteParser}. + */ +public class PickParserTest { + + @Test + public void testPickPositive() { + Parser parser = digit().seq(letter()).pick(1); + assertSuccess(parser, "1a", 'a'); + assertSuccess(parser, "2b", 'b'); + assertFailure(parser, "", 0, "digit expected"); + assertFailure(parser, "1", 1, "letter expected"); + assertFailure(parser, "12", 1, "letter expected"); + } + + @Test + public void testPickNegative() { + Parser parser = digit().seq(letter()).pick(-1); + assertSuccess(parser, "1a", 'a'); + assertSuccess(parser, "2b", 'b'); + + Parser first = digit().seq(letter()).pick(-2); + assertSuccess(first, "1a", '1'); + assertSuccess(first, "2b", '2'); + } + + @Test + public void testPickFastParse() { + Parser parser = digit().seq(letter()).pick(0); + assertEquals(2, parser.fastParseOn("1a", 0)); + assertEquals(-1, parser.fastParseOn("12", 0)); + assertEquals(3, parser.fastParseOn("x1a", 1)); + } + + @Test + public void testPickGetters() { + PickParser parser = new PickParser<>(digit().star(), 3); + assertEquals(3, parser.getIndex()); + } + + @Test + public void testPickCopyAndEquality() { + Parser parser = digit().seq(letter()).pick(1); + Parser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + Parser differentIndex = digit().seq(letter()).pick(0); + assertFalse(parser.isEqualTo(differentIndex)); + + Parser differentDelegate = letter().seq(digit()).pick(1); + assertFalse(parser.isEqualTo(differentDelegate)); + } + + @Test + public void testPickToString() { + Parser parser = digit().seq(letter()).pick(1); + assertEquals("PickParser[1]", parser.toString()); + } + + @Test + public void testPermuteFromStart() { + Parser parser = digit().seq(letter()).permute(1, 0); + assertSuccess(parser, "1a", Arrays.asList('a', '1')); + assertSuccess(parser, "2b", Arrays.asList('b', '2')); + assertFailure(parser, "", 0, "digit expected"); + assertFailure(parser, "1", 1, "letter expected"); + assertFailure(parser, "12", 1, "letter expected"); + + List result = parser.parse("1a").get(); + assertEquals('a', result.get(0)); + assertEquals('1', result.get(1)); + } + + @Test + public void testPermuteFromEnd() { + Parser parser = digit().seq(letter()).permute(-1, 0); + assertSuccess(parser, "1a", Arrays.asList('a', '1')); + assertSuccess(parser, "2b", Arrays.asList('b', '2')); + } + + @Test + public void testPermuteRepeated() { + Parser parser = digit().seq(letter()).permute(1, 1); + assertSuccess(parser, "1a", Arrays.asList('a', 'a')); + assertSuccess(parser, "2b", Arrays.asList('b', 'b')); + } + + @Test + public void testPermuteEmptyAndSingle() { + Parser emptyParser = digit().seq(letter()).permute(); + assertSuccess(emptyParser, "1a", Collections.emptyList()); + + Parser singleParser = digit().seq(letter()).permute(1); + assertSuccess(singleParser, "1a", Collections.singletonList('a')); + } + + @Test + public void testPermuteArities() { + // 3 elements returning Tuple3 + Parser p3 = Parser.seq(digit(), letter(), digit()).permute(2, 0, 1); + Object res3 = p3.parse("1a2").get(); + assertEquals(Arrays.asList('2', '1', 'a'), res3); + + // 4 elements returning Tuple4 + Parser p4 = digit().times(4).permute(3, 2, 1, 0); + Object res4 = p4.parse("1234").get(); + assertEquals(Arrays.asList('4', '3', '2', '1'), res4); + + // 5 elements returning Tuple5 + Parser p5 = digit().times(5).permute(4, 3, 2, 1, 0); + Object res5 = p5.parse("12345").get(); + assertEquals(Arrays.asList('5', '4', '3', '2', '1'), res5); + + // 6 elements returning Tuple6 + Parser p6 = digit().times(6).permute(5, 4, 3, 2, 1, 0); + Object res6 = p6.parse("123456").get(); + assertEquals(Arrays.asList('6', '5', '4', '3', '2', '1'), res6); + + // 7 elements returning Tuple7 + Parser p7 = digit().times(7).permute(6, 5, 4, 3, 2, 1, 0); + Object res7 = p7.parse("1234567").get(); + assertEquals(Arrays.asList('7', '6', '5', '4', '3', '2', '1'), res7); + + // 8 elements returning Tuple8 + Parser p8 = digit().times(8).permute(7, 6, 5, 4, 3, 2, 1, 0); + Object res8 = p8.parse("12345678").get(); + assertEquals(Arrays.asList('8', '7', '6', '5', '4', '3', '2', '1'), res8); + + // 9 elements returning Tuple9 + Parser p9 = digit().times(9).permute(8, 7, 6, 5, 4, 3, 2, 1, 0); + Object res9 = p9.parse("123456789").get(); + assertEquals(Arrays.asList('9', '8', '7', '6', '5', '4', '3', '2', '1'), res9); + + // 10 elements (>9 returns List) + Parser p10 = digit().times(10).permute(9, 8, 7, 6, 5, 4, 3, 2, 1, 0); + List res10 = p10.parse("0123456789").get(); + assertEquals(Arrays.asList('9', '8', '7', '6', '5', '4', '3', '2', '1', '0'), res10); + } + + @Test + public void testPermuteFastParse() { + Parser parser = digit().seq(letter()).permute(1, 0); + assertEquals(2, parser.fastParseOn("1a", 0)); + assertEquals(-1, parser.fastParseOn("11", 0)); + assertEquals(3, parser.fastParseOn("x1a", 1)); + } + + @Test + public void testPermuteGetters() { + PermuteParser parser = new PermuteParser(digit().star(), 1, -1, 0); + assertArrayEquals(new int[]{1, -1, 0}, parser.getIndices()); + } + + @Test + public void testPermuteCopyAndEquality() { + Parser parser = digit().seq(letter()).permute(1, 0); + Parser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + Parser differentIndices = digit().seq(letter()).permute(0, 1); + assertFalse(parser.isEqualTo(differentIndices)); + + Parser differentDelegate = letter().seq(digit()).permute(1, 0); + assertFalse(parser.isEqualTo(differentDelegate)); + } + + @Test + public void testPermuteToString() { + Parser parser = digit().seq(letter()).permute(1, 0); + assertEquals("PermuteParser[1, 0]", parser.toString()); + } + + @Test + public void testPermuteNegativeIndices() { + Parser singleNegative = digit().seq(letter()).permute(-1); + assertEquals(Collections.singletonList('a'), singleNegative.parse("1a").get()); + + Parser tenNegative = digit().repeat(10, 10).permute(-1, -2, -3, -4, -5, -6, -7, -8, -9, -10); + assertEquals( + Arrays.asList('9', '8', '7', '6', '5', '4', '3', '2', '1', '0'), + tenNegative.parse("0123456789").get()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/actions/WhereParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/actions/WhereParserTest.java new file mode 100644 index 0000000..7f4c86e --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/actions/WhereParserTest.java @@ -0,0 +1,165 @@ +package org.petitparser.parser.actions; + +import org.junit.Test; +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; + +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Predicate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; +import static org.petitparser.parser.primitive.CharacterParser.any; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.of; + +/** + * Tests for {@link WhereParser}. + */ +public class WhereParserTest { + + @Test + public void testDefault() { + Parser parser = any().where(c -> Objects.equals(c, '*')); + assertSuccess(parser, "*", '*'); + assertFailure(parser, "", 0, "any character expected"); + assertFailure(parser, "!", 0, "unexpected \"!\""); + } + + @Test + public void testWithMessage() { + Parser parser = any().where(c -> Objects.equals(c, '*'), "star expected"); + assertSuccess(parser, "*", '*'); + assertFailure(parser, "", 0, "any character expected"); + assertFailure(parser, "!", 0, "star expected"); + } + + @Test + public void testWithFactory() { + BiFunction factory = + (context, success) -> context.failure(success.get() + " is not divisible by 7"); + Parser parser = digit().plus().flatten() + .map((String s) -> Integer.parseInt(s)) + .where((Integer v) -> v % 7 == 0, factory); + assertSuccess(parser, "7", 7); + assertSuccess(parser, "14", 14); + assertSuccess(parser, "861", 861); + assertFailure(parser, "", 0, "digit expected"); + assertFailure(parser, "865", 0, "865 is not divisible by 7"); + } + + @Test + public void testSubsequencePosition() { + Parser parser = of('a').seq(of('b').where(c -> Objects.equals(c, 'x'))); + assertFailure(parser, "ab", 1, "unexpected \"b\""); + } + + @Test + public void testGetters() { + Predicate predicate = Objects::nonNull; + WhereParser parserDefault = new WhereParser<>(any(), predicate); + assertSame(predicate, parserDefault.getPredicate()); + assertNull(parserDefault.getMessage()); + assertNull(parserDefault.getFailureFactory()); + + WhereParser parser = new WhereParser<>(any(), predicate, "not null"); + assertSame(predicate, parser.getPredicate()); + assertEquals("not null", parser.getMessage()); + assertNull(parser.getFailureFactory()); + + BiFunction factory = (ctx, res) -> ctx.failure("custom"); + WhereParser parserWithFactory = new WhereParser<>(any(), predicate, factory); + assertSame(factory, parserWithFactory.getFailureFactory()); + assertNull(parserWithFactory.getMessage()); + } + + @Test + public void testNullChecks() { + try { + new WhereParser<>(null, Objects::nonNull); + fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + + try { + new WhereParser<>(any(), null); + fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + + try { + new WhereParser<>(any(), Objects::nonNull, (BiFunction) null); + fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testFastParse() { + Parser parser = of('a').where(c -> Objects.equals(c, 'a')); + assertEquals(1, parser.fastParseOn("a", 0)); + assertEquals(-1, parser.fastParseOn("b", 0)); + + Parser failingPredicate = of('a').where(c -> false); + assertEquals(-1, failingPredicate.fastParseOn("a", 0)); + } + + @Test + public void testCopyAndEquality() { + Predicate predicate = Objects::nonNull; + Parser parser = any().where(predicate, "err"); + Parser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + Parser differentDelegate = digit().where(predicate, "err"); + assertFalse(parser.isEqualTo(differentDelegate)); + + Parser differentPredicate = any().where(c -> true, "err"); + assertFalse(parser.isEqualTo(differentPredicate)); + + Parser differentMessage = any().where(predicate, "other"); + assertFalse(parser.isEqualTo(differentMessage)); + + BiFunction factory = (ctx, res) -> ctx.failure("factory"); + Parser withFactory = any().where(predicate, factory); + assertFalse(parser.isEqualTo(withFactory)); + + Parser withFactoryCopy = withFactory.copy(); + assertTrue(withFactory.isEqualTo(withFactoryCopy)); + + BiFunction otherFactory = (ctx, res) -> ctx.failure("other"); + Parser withOtherFactory = any().where(predicate, otherFactory); + assertFalse(withFactory.isEqualTo(withOtherFactory)); + } + + @Test + public void testToString() { + Parser defaultParser = any().where(Objects::nonNull); + assertEquals("WhereParser", defaultParser.toString()); + + Parser messageParser = any().where(Objects::nonNull, "custom"); + assertEquals("WhereParser[custom]", messageParser.toString()); + } + + @Test + public void testFastParseNonZeroOffset() { + Parser parser = of('a').where(c -> Objects.equals(c, 'a')); + assertEquals(2, parser.fastParseOn("ba", 1)); + assertEquals(-1, parser.fastParseOn("bb", 1)); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/combinators/ChoiceParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/combinators/ChoiceParserTest.java new file mode 100644 index 0000000..aacab3f --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/combinators/ChoiceParserTest.java @@ -0,0 +1,124 @@ +package org.petitparser.parser.combinators; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.utils.FailureJoiner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.letter; +import static org.petitparser.parser.primitive.CharacterParser.of; + +/** + * Tests for {@link ChoiceParser} including {@link Parser#orWiden} and static {@link Parser#or}. + */ +public class ChoiceParserTest { + + @Test + public void testStaticOr2() { + ChoiceParser parser = Parser.or(digit(), letter()); + assertSuccess(parser, "1", '1'); + assertSuccess(parser, "a", 'a'); + assertFailure(parser, "!", 0, "letter expected"); + } + + @Test + public void testStaticOr3() { + ChoiceParser parser = Parser.or(digit(), letter(), of('!')); + assertSuccess(parser, "1", '1'); + assertSuccess(parser, "a", 'a'); + assertSuccess(parser, "!", '!'); + assertFailure(parser, "?", 0, "'!' expected"); + } + + @Test + public void testStaticOrWithFailureJoiner() { + ChoiceParser parser = Parser.or(new FailureJoiner.SelectFirst(), digit(), letter()); + assertSuccess(parser, "1", '1'); + assertSuccess(parser, "a", 'a'); + assertFailure(parser, "!", 0, "digit expected"); + } + + @Test + public void testStaticOrArities4To9() { + ChoiceParser p4 = Parser.or(of('1'), of('2'), of('3'), of('4')); + assertSuccess(p4, "4", '4'); + + ChoiceParser p5 = Parser.or(of('1'), of('2'), of('3'), of('4'), of('5')); + assertSuccess(p5, "5", '5'); + + ChoiceParser p6 = Parser.or(of('1'), of('2'), of('3'), of('4'), of('5'), of('6')); + assertSuccess(p6, "6", '6'); + + ChoiceParser p7 = Parser.or(of('1'), of('2'), of('3'), of('4'), of('5'), of('6'), of('7')); + assertSuccess(p7, "7", '7'); + + ChoiceParser p8 = Parser.or(of('1'), of('2'), of('3'), of('4'), of('5'), of('6'), of('7'), of('8')); + assertSuccess(p8, "8", '8'); + + ChoiceParser p9 = Parser.or(of('1'), of('2'), of('3'), of('4'), of('5'), of('6'), of('7'), of('8'), of('9')); + assertSuccess(p9, "9", '9'); + assertFailure(p9, "0", 0, "'9' expected"); + } + + @Test + public void testStaticOrFailureJoinerArities3To9() { + FailureJoiner fj = new FailureJoiner.SelectFirst(); + assertSuccess(Parser.or(fj, of('1'), of('2'), of('3')), "3", '3'); + assertSuccess(Parser.or(fj, of('1'), of('2'), of('3'), of('4')), "4", '4'); + assertSuccess(Parser.or(fj, of('1'), of('2'), of('3'), of('4'), of('5')), "5", '5'); + assertSuccess(Parser.or(fj, of('1'), of('2'), of('3'), of('4'), of('5'), of('6')), "6", '6'); + assertSuccess(Parser.or(fj, of('1'), of('2'), of('3'), of('4'), of('5'), of('6'), of('7')), "7", '7'); + assertSuccess(Parser.or(fj, of('1'), of('2'), of('3'), of('4'), of('5'), of('6'), of('7'), of('8')), "8", '8'); + assertSuccess(Parser.or(fj, of('1'), of('2'), of('3'), of('4'), of('5'), of('6'), of('7'), of('8'), of('9')), "9", '9'); + } + + @Test + public void testOrWiden() { + Parser intParser = digit().map((Character c) -> (Number) Integer.valueOf(c.toString())); + Parser doubleParser = letter().map((Character c) -> (Number) Double.valueOf((int) c)); + ChoiceParser widened = intParser.orWiden(doubleParser); + + assertSuccess(widened, "1", 1); + assertSuccess(widened, "a", (double) (int) 'a'); + assertFailure(widened, "!", 0, "letter expected"); + } + + @Test + public void testOrWidenWithFailureJoiner() { + Parser intParser = digit().map((Character c) -> (Number) Integer.valueOf(c.toString())); + Parser doubleParser = letter().map((Character c) -> (Number) Double.valueOf((int) c)); + ChoiceParser widened = intParser.orWiden(new FailureJoiner.SelectFirst(), doubleParser); + + assertSuccess(widened, "1", 1); + assertSuccess(widened, "a", (double) (int) 'a'); + assertFailure(widened, "!", 0, "digit expected"); + } + + @Test + public void testFastParse() { + ChoiceParser parser = Parser.or(digit(), letter()); + assertEquals(1, parser.fastParseOn("1", 0)); + assertEquals(1, parser.fastParseOn("a", 0)); + assertEquals(-1, parser.fastParseOn("!", 0)); + assertEquals(2, parser.fastParseOn("!1", 1)); + } + + @Test + public void testCopyAndEquality() { + ChoiceParser parser = Parser.or(digit(), letter()); + ChoiceParser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + ChoiceParser different = Parser.or(letter(), digit()); + assertFalse(parser.isEqualTo(different)); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/combinators/EndOfInputParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/combinators/EndOfInputParserTest.java new file mode 100644 index 0000000..35fbd0f --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/combinators/EndOfInputParserTest.java @@ -0,0 +1,79 @@ +package org.petitparser.parser.combinators; + +import org.junit.Test; +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.primitive.CharacterParser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests {@link EndOfInputParser}. + */ +public class EndOfInputParserTest { + + @Test + public void testParseOn() { + EndOfInputParser parser = new EndOfInputParser("end expected"); + Result r1 = parser.parseOn(new Context("", 0)); + assertTrue(r1.isSuccess()); + assertNull(r1.get()); + assertEquals(0, r1.getPosition()); + + Result r2 = parser.parseOn(new Context("abc", 3)); + assertTrue(r2.isSuccess()); + assertNull(r2.get()); + assertEquals(3, r2.getPosition()); + + Result r3 = parser.parseOn(new Context("abc", 0)); + assertTrue(r3.isFailure()); + assertEquals("end expected", r3.getMessage()); + assertEquals(0, r3.getPosition()); + + Result r4 = parser.parseOn(new Context("abc", 2)); + assertTrue(r4.isFailure()); + assertEquals("end expected", r4.getMessage()); + assertEquals(2, r4.getPosition()); + } + + @Test + public void testFastParseOn() { + EndOfInputParser parser = new EndOfInputParser("end expected"); + assertEquals(0, parser.fastParseOn("", 0)); + assertEquals(3, parser.fastParseOn("abc", 3)); + assertEquals(-1, parser.fastParseOn("abc", 0)); + assertEquals(-1, parser.fastParseOn("abc", 1)); + } + + @Test(expected = NullPointerException.class) + public void testNullMessage() { + new EndOfInputParser(null); + } + + @Test + public void testCopyAndEquals() { + EndOfInputParser p1 = new EndOfInputParser("end expected"); + EndOfInputParser p2 = new EndOfInputParser("end expected"); + EndOfInputParser p3 = new EndOfInputParser("different message"); + + assertTrue(p1.isEqualTo(p2)); + assertFalse(p1.isEqualTo(p3)); + assertFalse(p1.isEqualTo(CharacterParser.any())); + + assertTrue(p1.hasEqualProperties(p2)); + assertFalse(p1.hasEqualProperties(p3)); + + EndOfInputParser copy = p1.copy(); + assertTrue(p1.isEqualTo(copy)); + assertEquals("end expected", copy.message); + } + + @Test + public void testToString() { + EndOfInputParser parser = new EndOfInputParser("end of file"); + assertTrue(parser.toString().contains("[end of file]")); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/combinators/LabelParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/combinators/LabelParserTest.java new file mode 100644 index 0000000..c260818 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/combinators/LabelParserTest.java @@ -0,0 +1,86 @@ +package org.petitparser.parser.combinators; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.letter; + +/** + * Tests for {@link LabelParser}. + */ +public class LabelParserTest { + + @Test + public void testLabel() { + LabelParser parser = (LabelParser) digit().labeled("number"); + assertEquals("number", parser.getLabel()); + assertEquals(digit().toString() + "[number]", parser.toString()); + + assertSuccess(parser, "1", '1'); + assertFailure(parser, "a", 0, "digit expected"); + } + + @Test + public void testFastParse() { + Parser parser = digit().labeled("number"); + assertEquals(1, parser.fastParseOn("1", 0)); + assertEquals(2, parser.fastParseOn("a1", 1)); + assertEquals(-1, parser.fastParseOn("a", 0)); + } + + @Test + public void testNullChecks() { + try { + new LabelParser(null, "label"); + fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + + try { + new LabelParser(digit(), null); + fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testCopyAndEquality() { + Parser parser = digit().labeled("number"); + Parser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + Parser differentLabel = digit().labeled("digit"); + assertFalse(parser.isEqualTo(differentLabel)); + + Parser differentDelegate = letter().labeled("number"); + assertFalse(parser.isEqualTo(differentDelegate)); + } + + @Test + public void testReplace() { + Parser inner = digit(); + LabelParser parser = (LabelParser) inner.labeled("number"); + Parser newDelegate = letter(); + + parser.replace(inner, newDelegate); + assertSame(newDelegate, parser.getChildren().get(0)); + + parser.replace(inner, CharacterParser.any()); + assertSame(newDelegate, parser.getChildren().get(0)); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/combinators/SequenceParserNTest.java b/petitparser-core/src/test/java/org/petitparser/parser/combinators/SequenceParserNTest.java new file mode 100644 index 0000000..9356080 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/combinators/SequenceParserNTest.java @@ -0,0 +1,493 @@ +package org.petitparser.parser.combinators; + +import org.junit.Test; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.utils.tuples.Tuple2; +import org.petitparser.utils.tuples.Tuple3; +import org.petitparser.utils.tuples.Tuple4; +import org.petitparser.utils.tuples.Tuple5; +import org.petitparser.utils.tuples.Tuple6; +import org.petitparser.utils.tuples.Tuple7; +import org.petitparser.utils.tuples.Tuple8; +import org.petitparser.utils.tuples.Tuple9; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.primitive.CharacterParser.of; + +/** + * Tests {@link SequenceParser2} through {@link SequenceParser9}. + */ +public class SequenceParserNTest { + + @Test + public void testSequenceParser2() { + SequenceParser2 parser = of('a').then(of('b')); + Result result = parser.parse("ab"); + assertTrue(result.isSuccess()); + Tuple2 tuple = result.get(); + assertEquals(Character.valueOf('a'), tuple.first()); + assertEquals(Character.valueOf('b'), tuple.second()); + assertEquals(Arrays.asList('a', 'b'), result.get()); + + assertTrue(parser.accept("ab")); + assertFalse(parser.accept("xb")); + assertFalse(parser.accept("ax")); + assertFalse(parser.accept("")); + + assertFalse(parser.parse("xb").isSuccess()); + assertFalse(parser.parse("ax").isSuccess()); + assertFalse(parser.parse("").isSuccess()); + + assertEquals(2, parser.fastParseOn("abc", 0)); + assertEquals(-1, parser.fastParseOn("xbc", 0)); + assertEquals(-1, parser.fastParseOn("axc", 0)); + + Parser copy = parser.copy(); + assertTrue(parser.isEqualTo(copy)); + + Parser mapped = parser.map((c1, c2) -> "" + c1 + c2); + assertEquals("ab", mapped.parse("ab").get()); + } + + @Test(expected = NullPointerException.class) + public void testSequenceParser2NullMap() { + Parser.seq(of('a'), of('b')).map((java.util.function.BiFunction) null); + } + + @Test + public void testSequenceParser3() { + SequenceParser3 parser = + of('a').then(of('b')).then(of('c')); + Result result = parser.parse("abc"); + assertTrue(result.isSuccess()); + Tuple3 tuple = result.get(); + assertEquals(Character.valueOf('a'), tuple.first()); + assertEquals(Character.valueOf('b'), tuple.second()); + assertEquals(Character.valueOf('c'), tuple.third()); + assertEquals(Arrays.asList('a', 'b', 'c'), result.get()); + + assertTrue(parser.accept("abc")); + assertFalse(parser.accept("xbc")); + assertFalse(parser.accept("axc")); + assertFalse(parser.accept("abx")); + + assertFalse(parser.parse("xbc").isSuccess()); + assertFalse(parser.parse("axc").isSuccess()); + assertFalse(parser.parse("abx").isSuccess()); + assertFalse(parser.parse("").isSuccess()); + + assertEquals(3, parser.fastParseOn("abcd", 0)); + assertEquals(-1, parser.fastParseOn("xbcd", 0)); + assertEquals(-1, parser.fastParseOn("axcd", 0)); + assertEquals(-1, parser.fastParseOn("abxd", 0)); + + Parser copy = parser.copy(); + assertTrue(parser.isEqualTo(copy)); + + Parser mapped = parser.map((c1, c2, c3) -> "" + c1 + c2 + c3); + assertEquals("abc", mapped.parse("abc").get()); + } + + @Test(expected = NullPointerException.class) + public void testSequenceParser3NullMap() { + Parser.seq(of('a'), of('b'), of('c')).map((org.petitparser.utils.functions.Function3) null); + } + + @Test + public void testSequenceParser4() { + SequenceParser4 parser = + of('a').then(of('b')).then(of('c')).then(of('d')); + Result result = parser.parse("abcd"); + assertTrue(result.isSuccess()); + Tuple4 tuple = result.get(); + assertEquals(Character.valueOf('a'), tuple.first()); + assertEquals(Character.valueOf('b'), tuple.second()); + assertEquals(Character.valueOf('c'), tuple.third()); + assertEquals(Character.valueOf('d'), tuple.fourth()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd'), result.get()); + + assertTrue(parser.accept("abcd")); + assertFalse(parser.accept("xbcd")); + assertFalse(parser.accept("axcd")); + assertFalse(parser.accept("abxd")); + assertFalse(parser.accept("abcx")); + + assertFalse(parser.parse("xbcd").isSuccess()); + assertFalse(parser.parse("axcd").isSuccess()); + assertFalse(parser.parse("abxd").isSuccess()); + assertFalse(parser.parse("abcx").isSuccess()); + assertFalse(parser.parse("").isSuccess()); + + assertEquals(4, parser.fastParseOn("abcde", 0)); + assertEquals(-1, parser.fastParseOn("xbcde", 0)); + assertEquals(-1, parser.fastParseOn("axcde", 0)); + assertEquals(-1, parser.fastParseOn("abxde", 0)); + assertEquals(-1, parser.fastParseOn("abcxe", 0)); + + Parser copy = parser.copy(); + assertTrue(parser.isEqualTo(copy)); + + Parser mapped = parser.map((c1, c2, c3, c4) -> "" + c1 + c2 + c3 + c4); + assertEquals("abcd", mapped.parse("abcd").get()); + } + + @Test(expected = NullPointerException.class) + public void testSequenceParser4NullMap() { + Parser.seq(of('a'), of('b'), of('c'), of('d')).map((org.petitparser.utils.functions.Function4) null); + } + + @Test + public void testSequenceParser5() { + SequenceParser5 parser = + of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')); + Result result = parser.parse("abcde"); + assertTrue(result.isSuccess()); + Tuple5 tuple = result.get(); + assertEquals(Character.valueOf('a'), tuple.first()); + assertEquals(Character.valueOf('e'), tuple.fifth()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e'), result.get()); + + assertTrue(parser.accept("abcde")); + assertFalse(parser.accept("abcdx")); + + assertFalse(parser.parse("xbcde").isSuccess()); + assertFalse(parser.parse("axcde").isSuccess()); + assertFalse(parser.parse("abxde").isSuccess()); + assertFalse(parser.parse("abcxe").isSuccess()); + assertFalse(parser.parse("abcdx").isSuccess()); + assertFalse(parser.parse("").isSuccess()); + + assertEquals(5, parser.fastParseOn("abcdef", 0)); + assertEquals(-1, parser.fastParseOn("xbcdef", 0)); + assertEquals(-1, parser.fastParseOn("axcdef", 0)); + assertEquals(-1, parser.fastParseOn("abxdef", 0)); + assertEquals(-1, parser.fastParseOn("abcxef", 0)); + assertEquals(-1, parser.fastParseOn("abcdxf", 0)); + + Parser copy = parser.copy(); + assertTrue(parser.isEqualTo(copy)); + + Parser mapped = parser.map((c1, c2, c3, c4, c5) -> "" + c1 + c2 + c3 + c4 + c5); + assertEquals("abcde", mapped.parse("abcde").get()); + } + + @Test(expected = NullPointerException.class) + public void testSequenceParser5NullMap() { + Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e')).map((org.petitparser.utils.functions.Function5) null); + } + + @Test + public void testSequenceParser6() { + SequenceParser6 parser = + of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')).then(of('f')); + Result result = parser.parse("abcdef"); + assertTrue(result.isSuccess()); + Tuple6 tuple = result.get(); + assertEquals(Character.valueOf('a'), tuple.first()); + assertEquals(Character.valueOf('f'), tuple.sixth()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e', 'f'), result.get()); + + assertTrue(parser.accept("abcdef")); + assertFalse(parser.accept("abcdex")); + + assertFalse(parser.parse("xbcdef").isSuccess()); + assertFalse(parser.parse("axcdef").isSuccess()); + assertFalse(parser.parse("abxdef").isSuccess()); + assertFalse(parser.parse("abcxef").isSuccess()); + assertFalse(parser.parse("abcdxf").isSuccess()); + assertFalse(parser.parse("abcdex").isSuccess()); + assertFalse(parser.parse("").isSuccess()); + + assertEquals(6, parser.fastParseOn("abcdefg", 0)); + assertEquals(-1, parser.fastParseOn("xbcdefg", 0)); + assertEquals(-1, parser.fastParseOn("axcdefg", 0)); + assertEquals(-1, parser.fastParseOn("abxdefg", 0)); + assertEquals(-1, parser.fastParseOn("abcxefg", 0)); + assertEquals(-1, parser.fastParseOn("abcdxfg", 0)); + assertEquals(-1, parser.fastParseOn("abcdexg", 0)); + + Parser copy = parser.copy(); + assertTrue(parser.isEqualTo(copy)); + + Parser mapped = parser.map((c1, c2, c3, c4, c5, c6) -> "" + c1 + c2 + c3 + c4 + c5 + c6); + assertEquals("abcdef", mapped.parse("abcdef").get()); + } + + @Test(expected = NullPointerException.class) + public void testSequenceParser6NullMap() { + Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e'), of('f')).map((org.petitparser.utils.functions.Function6) null); + } + + @Test + public void testSequenceParser7() { + SequenceParser7 parser = + of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')).then(of('f')).then(of('g')); + Result result = parser.parse("abcdefg"); + assertTrue(result.isSuccess()); + Tuple7 tuple = result.get(); + assertEquals(Character.valueOf('a'), tuple.first()); + assertEquals(Character.valueOf('g'), tuple.seventh()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e', 'f', 'g'), result.get()); + + assertTrue(parser.accept("abcdefg")); + assertFalse(parser.accept("abcdefx")); + + assertFalse(parser.parse("xbcdefg").isSuccess()); + assertFalse(parser.parse("axcdefg").isSuccess()); + assertFalse(parser.parse("abxdefg").isSuccess()); + assertFalse(parser.parse("abcxefg").isSuccess()); + assertFalse(parser.parse("abcdxfg").isSuccess()); + assertFalse(parser.parse("abcdexg").isSuccess()); + assertFalse(parser.parse("abcdefx").isSuccess()); + assertFalse(parser.parse("").isSuccess()); + + assertEquals(7, parser.fastParseOn("abcdefgh", 0)); + assertEquals(-1, parser.fastParseOn("xbcdefgh", 0)); + assertEquals(-1, parser.fastParseOn("axcdefgh", 0)); + assertEquals(-1, parser.fastParseOn("abxdefgh", 0)); + assertEquals(-1, parser.fastParseOn("abcxefgh", 0)); + assertEquals(-1, parser.fastParseOn("abcdxfgh", 0)); + assertEquals(-1, parser.fastParseOn("abcdexgh", 0)); + assertEquals(-1, parser.fastParseOn("abcdefxh", 0)); + + Parser copy = parser.copy(); + assertTrue(parser.isEqualTo(copy)); + + Parser mapped = parser.map((c1, c2, c3, c4, c5, c6, c7) -> "" + c1 + c2 + c3 + c4 + c5 + c6 + c7); + assertEquals("abcdefg", mapped.parse("abcdefg").get()); + } + + @Test(expected = NullPointerException.class) + public void testSequenceParser7NullMap() { + Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e'), of('f'), of('g')).map((org.petitparser.utils.functions.Function7) null); + } + + @Test + public void testSequenceParser8() { + SequenceParser8 parser = + of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')).then(of('f')).then(of('g')).then(of('h')); + Result result = parser.parse("abcdefgh"); + assertTrue(result.isSuccess()); + Tuple8 tuple = result.get(); + assertEquals(Character.valueOf('a'), tuple.first()); + assertEquals(Character.valueOf('h'), tuple.eighth()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), result.get()); + + assertTrue(parser.accept("abcdefgh")); + assertFalse(parser.accept("abcdefgx")); + + assertFalse(parser.parse("xbcdefgh").isSuccess()); + assertFalse(parser.parse("axcdefgh").isSuccess()); + assertFalse(parser.parse("abxdefgh").isSuccess()); + assertFalse(parser.parse("abcxefgh").isSuccess()); + assertFalse(parser.parse("abcdxfgh").isSuccess()); + assertFalse(parser.parse("abcdexgh").isSuccess()); + assertFalse(parser.parse("abcdefxh").isSuccess()); + assertFalse(parser.parse("abcdefgx").isSuccess()); + assertFalse(parser.parse("").isSuccess()); + + assertEquals(8, parser.fastParseOn("abcdefghi", 0)); + assertEquals(-1, parser.fastParseOn("xbcdefghi", 0)); + assertEquals(-1, parser.fastParseOn("axcdefghi", 0)); + assertEquals(-1, parser.fastParseOn("abxdefghi", 0)); + assertEquals(-1, parser.fastParseOn("abcxefghi", 0)); + assertEquals(-1, parser.fastParseOn("abcdxfghi", 0)); + assertEquals(-1, parser.fastParseOn("abcdexghi", 0)); + assertEquals(-1, parser.fastParseOn("abcdefxhi", 0)); + assertEquals(-1, parser.fastParseOn("abcdefgxi", 0)); + + Parser copy = parser.copy(); + assertTrue(parser.isEqualTo(copy)); + + Parser mapped = parser.map((c1, c2, c3, c4, c5, c6, c7, c8) -> + "" + c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8); + assertEquals("abcdefgh", mapped.parse("abcdefgh").get()); + } + + @Test(expected = NullPointerException.class) + public void testSequenceParser8NullMap() { + Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e'), of('f'), of('g'), of('h')).map((org.petitparser.utils.functions.Function8) null); + } + + @Test + public void testSequenceParser9() { + SequenceParser9 parser = + of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')) + .then(of('f')).then(of('g')).then(of('h')).then(of('i')); + Result result = parser.parse("abcdefghi"); + assertTrue(result.isSuccess()); + Tuple9 tuple = result.get(); + assertEquals(Character.valueOf('a'), tuple.first()); + assertEquals(Character.valueOf('i'), tuple.ninth()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'), result.get()); + + assertTrue(parser.accept("abcdefghi")); + assertFalse(parser.accept("abcdefghx")); + + assertFalse(parser.parse("xbcdefghi").isSuccess()); + assertFalse(parser.parse("axcdefghi").isSuccess()); + assertFalse(parser.parse("abxdefghi").isSuccess()); + assertFalse(parser.parse("abcxefghi").isSuccess()); + assertFalse(parser.parse("abcdxfghi").isSuccess()); + assertFalse(parser.parse("abcdexghi").isSuccess()); + assertFalse(parser.parse("abcdefxhi").isSuccess()); + assertFalse(parser.parse("abcdefgxi").isSuccess()); + assertFalse(parser.parse("abcdefghx").isSuccess()); + assertFalse(parser.parse("").isSuccess()); + + assertEquals(9, parser.fastParseOn("abcdefghij", 0)); + assertEquals(-1, parser.fastParseOn("xbcdefghij", 0)); + assertEquals(-1, parser.fastParseOn("axcdefghij", 0)); + assertEquals(-1, parser.fastParseOn("abxdefghij", 0)); + assertEquals(-1, parser.fastParseOn("abcxefghij", 0)); + assertEquals(-1, parser.fastParseOn("abcdxfghij", 0)); + assertEquals(-1, parser.fastParseOn("abcdexghij", 0)); + assertEquals(-1, parser.fastParseOn("abcdefxhij", 0)); + assertEquals(-1, parser.fastParseOn("abcdefgxij", 0)); + assertEquals(-1, parser.fastParseOn("abcdefghxj", 0)); + + Parser copy = parser.copy(); + assertTrue(parser.isEqualTo(copy)); + + Parser mapped = parser.map((c1, c2, c3, c4, c5, c6, c7, c8, c9) -> + "" + c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9); + assertEquals("abcdefghi", mapped.parse("abcdefghi").get()); + + // Test then beyond 9 elements falls back to SequenceParser + SequenceParser seq10 = parser.then(of('j')); + assertEquals(10, seq10.getChildren().size()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), + seq10.parse("abcdefghij").get()); + } + + @Test(expected = NullPointerException.class) + public void testSequenceParser9NullMap() { + Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e'), of('f'), of('g'), of('h'), of('i')).map((org.petitparser.utils.functions.Function9) null); + } + + @Test + public void testStaticSeqFactories() { + assertEquals(Arrays.asList('a', 'b'), Parser.seq(of('a'), of('b')).parse("ab").get()); + assertEquals(Arrays.asList('a', 'b', 'c'), Parser.seq(of('a'), of('b'), of('c')).parse("abc").get()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd'), Parser.seq(of('a'), of('b'), of('c'), of('d')).parse("abcd").get()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e'), Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e')).parse("abcde").get()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e', 'f'), Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e'), of('f')).parse("abcdef").get()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e', 'f', 'g'), Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e'), of('f'), of('g')).parse("abcdefg").get()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e'), of('f'), of('g'), of('h')).parse("abcdefgh").get()); + assertEquals(Arrays.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'), Parser.seq(of('a'), of('b'), of('c'), of('d'), of('e'), of('f'), of('g'), of('h'), of('i')).parse("abcdefghi").get()); + assertEquals(Arrays.asList('a'), new SequenceParser(of('a')).parse("a").get()); + assertEquals(Arrays.asList('a', 'b'), of('a').seq(of('b')).parse("ab").get()); + } + + @Test + public void testDirectFluentChainingAndMapping() { + Parser p2 = of('a').then(of('b')).map((c1, c2) -> "" + c1 + c2); + assertEquals("ab", p2.parse("ab").get()); + + Parser p3 = of('a').then(of('b')).then(of('c')) + .map((c1, c2, c3) -> "" + c1 + c2 + c3); + assertEquals("abc", p3.parse("abc").get()); + + Parser p4 = of('a').then(of('b')).then(of('c')).then(of('d')) + .map((c1, c2, c3, c4) -> "" + c1 + c2 + c3 + c4); + assertEquals("abcd", p4.parse("abcd").get()); + + Parser p5 = of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')) + .map((c1, c2, c3, c4, c5) -> "" + c1 + c2 + c3 + c4 + c5); + assertEquals("abcde", p5.parse("abcde").get()); + + Parser p6 = of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')).then(of('f')) + .map((c1, c2, c3, c4, c5, c6) -> "" + c1 + c2 + c3 + c4 + c5 + c6); + assertEquals("abcdef", p6.parse("abcdef").get()); + + Parser p7 = of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')).then(of('f')).then(of('g')) + .map((c1, c2, c3, c4, c5, c6, c7) -> "" + c1 + c2 + c3 + c4 + c5 + c6 + c7); + assertEquals("abcdefg", p7.parse("abcdefg").get()); + + Parser p8 = of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')).then(of('f')).then(of('g')).then(of('h')) + .map((c1, c2, c3, c4, c5, c6, c7, c8) -> "" + c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8); + assertEquals("abcdefgh", p8.parse("abcdefgh").get()); + + Parser p9 = of('a').then(of('b')).then(of('c')).then(of('d')).then(of('e')).then(of('f')).then(of('g')).then(of('h')).then(of('i')) + .map((c1, c2, c3, c4, c5, c6, c7, c8, c9) -> "" + c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9); + assertEquals("abcdefghi", p9.parse("abcdefghi").get()); + } + + @Test + public void testUntypedSequenceParserMultiArityMap() { + Parser p2 = new SequenceParser(of('a'), of('b')).map((c1, c2) -> "" + c1 + c2); + assertEquals("ab", p2.parse("ab").get()); + + Parser p3 = new SequenceParser(of('a'), of('b'), of('c')).map((c1, c2, c3) -> "" + c1 + c2 + c3); + assertEquals("abc", p3.parse("abc").get()); + + Parser p4 = new SequenceParser(of('a'), of('b'), of('c'), of('d')) + .map((c1, c2, c3, c4) -> "" + c1 + c2 + c3 + c4); + assertEquals("abcd", p4.parse("abcd").get()); + + Parser p5 = new SequenceParser(of('a'), of('b'), of('c'), of('d'), of('e')) + .map((c1, c2, c3, c4, c5) -> "" + c1 + c2 + c3 + c4 + c5); + assertEquals("abcde", p5.parse("abcde").get()); + + Parser p6 = new SequenceParser(of('a'), of('b'), of('c'), of('d'), of('e'), of('f')) + .map((c1, c2, c3, c4, c5, c6) -> "" + c1 + c2 + c3 + c4 + c5 + c6); + assertEquals("abcdef", p6.parse("abcdef").get()); + + Parser p7 = new SequenceParser(of('a'), of('b'), of('c'), of('d'), of('e'), of('f'), of('g')) + .map((c1, c2, c3, c4, c5, c6, c7) -> "" + c1 + c2 + c3 + c4 + c5 + c6 + c7); + assertEquals("abcdefg", p7.parse("abcdefg").get()); + + Parser p8 = new SequenceParser(of('a'), of('b'), of('c'), of('d'), of('e'), of('f'), of('g'), of('h')) + .map((c1, c2, c3, c4, c5, c6, c7, c8) -> "" + c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8); + assertEquals("abcdefgh", p8.parse("abcdefgh").get()); + + Parser p9 = new SequenceParser(of('a'), of('b'), of('c'), of('d'), of('e'), of('f'), of('g'), of('h'), of('i')) + .map((c1, c2, c3, c4, c5, c6, c7, c8, c9) -> "" + c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9); + assertEquals("abcdefghi", p9.parse("abcdefghi").get()); + } + + @Test(expected = NullPointerException.class) + public void testNullBiFunctionMap() { + of('a').map((java.util.function.BiFunction) null); + } + + @Test(expected = NullPointerException.class) + public void testNullFunction4Map() { + of('a').map((org.petitparser.utils.functions.Function4) null); + } + + @Test(expected = NullPointerException.class) + public void testNullFunction5Map() { + of('a').map((org.petitparser.utils.functions.Function5) null); + } + + @Test(expected = NullPointerException.class) + public void testNullFunction6Map() { + of('a').map((org.petitparser.utils.functions.Function6) null); + } + + @Test(expected = NullPointerException.class) + public void testNullFunction7Map() { + of('a').map((org.petitparser.utils.functions.Function7) null); + } + + @Test(expected = NullPointerException.class) + public void testNullFunction3Map() { + of('a').map((org.petitparser.utils.functions.Function3) null); + } + + @Test(expected = NullPointerException.class) + public void testNullFunction8Map() { + of('a').map((org.petitparser.utils.functions.Function8) null); + } + + @Test(expected = NullPointerException.class) + public void testNullFunction9Map() { + of('a').map((org.petitparser.utils.functions.Function9) null); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/combinators/SettableParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/combinators/SettableParserTest.java new file mode 100644 index 0000000..fe5e37f --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/combinators/SettableParserTest.java @@ -0,0 +1,38 @@ +package org.petitparser.parser.combinators; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; + +/** + * Tests for {@link SettableParser}, {@link ResolvableParser}, and {@link SequentialParser}. + */ +public class SettableParserTest { + + @Test + public void testSettable() { + SettableParser parser = SettableParser.undefined(); + assertTrue(parser instanceof ResolvableParser); + assertFailure(parser, "a", 0, "Undefined parser"); + + CharacterParser delegate = CharacterParser.of('a'); + parser.set(delegate); + assertSame(delegate, parser.get()); + assertSame(delegate, parser.resolve()); + assertSuccess(parser, "a", 'a'); + } + + @Test + public void testSequentialMarker() { + Parser sequence = CharacterParser.of('a').seq(CharacterParser.of('b')); + assertTrue(sequence instanceof SequentialParser); + + Parser trimming = CharacterParser.of('a').trim(); + assertTrue(trimming instanceof SequentialParser); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/combinators/SkipParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/combinators/SkipParserTest.java new file mode 100644 index 0000000..7c78d7b --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/combinators/SkipParserTest.java @@ -0,0 +1,151 @@ +package org.petitparser.parser.combinators; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.EpsilonParser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.of; + +/** + * Tests for {@link SkipParser}. + */ +public class SkipParserTest { + + private final Parser inner = digit(); + private final Parser before = of('<'); + private final Parser after = of('>'); + + @Test + public void testSequentialMarker() { + Parser parser = inner.skip(before, after); + assertTrue(parser instanceof SequentialParser); + } + + @Test + public void testNone() { + SkipParser parser = new SkipParser(inner); + assertSame(EpsilonParser.INSTANCE, parser.getBefore()); + assertSame(EpsilonParser.INSTANCE, parser.getAfter()); + assertSame(inner, parser.getChildren().get(1)); + + assertSuccess(parser, "1", '1'); + assertSuccess(parser, "2", '2'); + assertFailure(parser, "", 0, "digit expected"); + } + + @Test + public void testBefore() { + SkipParser parser = (SkipParser) inner.skip(before, null); + assertSame(before, parser.getBefore()); + assertSame(EpsilonParser.INSTANCE, parser.getAfter()); + + assertSuccess(parser, "<1", '1'); + assertSuccess(parser, "<2", '2'); + assertFailure(parser, "", 0, "'<' expected"); + assertFailure(parser, "1", 0, "'<' expected"); + assertFailure(parser, "<", 1, "digit expected"); + assertFailure(parser, "", '1'); + assertSuccess(parser, "2>", '2'); + assertFailure(parser, "", 0, "digit expected"); + assertFailure(parser, "1", 1, "'>' expected"); + assertFailure(parser, "1!", 1, "'>' expected"); + assertFailure(parser, ">", 0, "digit expected"); + assertFailure(parser, "a>", 0, "digit expected"); + } + + @Test + public void testBeforeAndAfter() { + SkipParser parser = (SkipParser) inner.skip(before, after); + assertSame(before, parser.getBefore()); + assertSame(after, parser.getAfter()); + + assertSuccess(parser, "<1>", '1'); + assertSuccess(parser, "<2>", '2'); + assertFailure(parser, "", 0, "'<' expected"); + assertFailure(parser, "1", 0, "'<' expected"); + assertFailure(parser, "1>", 0, "'<' expected"); + assertFailure(parser, "1!", 0, "'<' expected"); + assertFailure(parser, "<", 1, "digit expected"); + assertFailure(parser, "<1", 2, "'>' expected"); + assertFailure(parser, "<1!", 2, "'>' expected"); + } + + @Test + public void testFastParse() { + Parser parser = inner.skip(before, after); + assertEquals(3, parser.fastParseOn("<1>", 0)); + assertEquals(-1, parser.fastParseOn("<1", 0)); + assertEquals(-1, parser.fastParseOn("1>", 0)); + assertEquals(-1, parser.fastParseOn(" ranges = Arrays.asList( + new RangeCharPredicate('a', 'c'), + new RangeCharPredicate('x', 'z')); + LookupCharPredicate predicate = LookupCharPredicate.fromRanges(ranges); + + assertEquals('a', predicate.getStart()); + assertEquals('z', predicate.getStop()); + assertTrue(predicate.getBits().length > 0); + + assertTrue(predicate.test('a')); + assertTrue(predicate.test('b')); + assertTrue(predicate.test('c')); + assertFalse(predicate.test('d')); + assertTrue(predicate.test('x')); + assertTrue(predicate.test('y')); + assertTrue(predicate.test('z')); + assertFalse(predicate.test('w')); + assertFalse(predicate.test((char) ('a' - 1))); + assertFalse(predicate.test((char) ('z' + 1))); + + LookupCharPredicate same = LookupCharPredicate.fromRanges(ranges); + LookupCharPredicate different = LookupCharPredicate.fromRanges( + Arrays.asList(new RangeCharPredicate('a', 'c'))); + + assertTrue(predicate.isEqualTo(same)); + assertFalse(predicate.isEqualTo(different)); + assertEquals(predicate, same); + assertNotEquals(predicate, different); + assertEquals(predicate.hashCode(), same.hashCode()); + assertTrue(predicate.toString().contains("97..122")); + + boolean[] table = new boolean[256]; + table['a'] = true; + table['b'] = true; + LookupCharPredicate fromTable = new LookupCharPredicate(table); + assertTrue(fromTable.test('a')); + assertTrue(fromTable.test('b')); + assertFalse(fromTable.test('c')); + } + + @Test(expected = IllegalArgumentException.class) + public void testLookupCharPredicateEmpty() { + LookupCharPredicate.fromRanges(List.of()); + } + + @Test(expected = IllegalArgumentException.class) + public void testLookupCharPredicateInvalidRange() { + new LookupCharPredicate(10, 5, new int[]{0}); + } + + @Test + public void testNotCharPredicate() { + CharacterPredicate inner = new SingleCharPredicate('a'); + CharacterPredicate not = inner.not(); + + assertTrue(not instanceof NotCharPredicate); + assertSame(inner, ((NotCharPredicate) not).getPredicate()); + assertFalse(not.test('a')); + assertTrue(not.test('b')); + assertTrue(not.test((int) 'b')); + + assertSame(inner, not.not()); + + CharacterPredicate same = new SingleCharPredicate('a').not(); + CharacterPredicate different = new SingleCharPredicate('b').not(); + + assertTrue(not.isEqualTo(same)); + assertFalse(not.isEqualTo(different)); + assertEquals(not, same); + assertNotEquals(not, different); + assertEquals(not.hashCode(), same.hashCode()); + assertTrue(not.toString().contains("NotCharPredicate")); + } + + @Test + public void testConstantCharPredicate() { + ConstantCharPredicate any = ConstantCharPredicate.any(); + ConstantCharPredicate none = ConstantCharPredicate.none(); + + assertTrue(any.getResult()); + assertFalse(none.getResult()); + + assertTrue(any.test('x')); + assertTrue(any.test(12345)); + assertFalse(none.test('x')); + assertFalse(none.test(12345)); + + assertSame(none, any.not()); + assertSame(any, none.not()); + + assertTrue(any.isEqualTo(ConstantCharPredicate.ANY)); + assertFalse(any.isEqualTo(none)); + assertEquals(any, ConstantCharPredicate.ANY); + assertNotEquals(any, none); + assertEquals(any.hashCode(), ConstantCharPredicate.ANY.hashCode()); + assertTrue(any.toString().contains("true")); + } + + @Test + public void testSingletons() { + // Digit + assertTrue(DigitCharPredicate.INSTANCE.test('5')); + assertFalse(DigitCharPredicate.INSTANCE.test('a')); + assertTrue(DigitCharPredicate.INSTANCE.test((int) '5')); + assertTrue(DigitCharPredicate.INSTANCE.isEqualTo(new DigitCharPredicate())); + assertEquals(DigitCharPredicate.INSTANCE, new DigitCharPredicate()); + assertEquals("DigitCharPredicate", DigitCharPredicate.INSTANCE.toString()); + + // Letter + assertTrue(LetterCharPredicate.INSTANCE.test('a')); + assertTrue(LetterCharPredicate.INSTANCE.test('Z')); + assertFalse(LetterCharPredicate.INSTANCE.test('5')); + assertTrue(LetterCharPredicate.INSTANCE.test((int) 'a')); + assertTrue(LetterCharPredicate.INSTANCE.isEqualTo(new LetterCharPredicate())); + assertEquals(LetterCharPredicate.INSTANCE, new LetterCharPredicate()); + assertEquals("LetterCharPredicate", LetterCharPredicate.INSTANCE.toString()); + + // Lowercase + assertTrue(LowercaseCharPredicate.INSTANCE.test('a')); + assertFalse(LowercaseCharPredicate.INSTANCE.test('A')); + assertTrue(LowercaseCharPredicate.INSTANCE.test((int) 'a')); + assertTrue(LowercaseCharPredicate.INSTANCE.isEqualTo(new LowercaseCharPredicate())); + assertEquals(LowercaseCharPredicate.INSTANCE, new LowercaseCharPredicate()); + assertEquals("LowercaseCharPredicate", LowercaseCharPredicate.INSTANCE.toString()); + + // Uppercase + assertTrue(UppercaseCharPredicate.INSTANCE.test('A')); + assertFalse(UppercaseCharPredicate.INSTANCE.test('a')); + assertTrue(UppercaseCharPredicate.INSTANCE.test((int) 'A')); + assertTrue(UppercaseCharPredicate.INSTANCE.isEqualTo(new UppercaseCharPredicate())); + assertEquals(UppercaseCharPredicate.INSTANCE, new UppercaseCharPredicate()); + assertEquals("UppercaseCharPredicate", UppercaseCharPredicate.INSTANCE.toString()); + + // Whitespace + assertTrue(WhitespaceCharPredicate.INSTANCE.test(' ')); + assertTrue(WhitespaceCharPredicate.INSTANCE.test('\t')); + assertFalse(WhitespaceCharPredicate.INSTANCE.test('a')); + assertTrue(WhitespaceCharPredicate.INSTANCE.test((int) ' ')); + assertTrue(WhitespaceCharPredicate.INSTANCE.isEqualTo(new WhitespaceCharPredicate())); + assertEquals(WhitespaceCharPredicate.INSTANCE, new WhitespaceCharPredicate()); + assertEquals("WhitespaceCharPredicate", WhitespaceCharPredicate.INSTANCE.toString()); + + // Word + assertTrue(WordCharPredicate.INSTANCE.test('a')); + assertTrue(WordCharPredicate.INSTANCE.test('9')); + assertFalse(WordCharPredicate.INSTANCE.test(' ')); + assertTrue(WordCharPredicate.INSTANCE.test((int) 'a')); + assertTrue(WordCharPredicate.INSTANCE.isEqualTo(new WordCharPredicate())); + assertEquals(WordCharPredicate.INSTANCE, new WordCharPredicate()); + assertEquals("WordCharPredicate", WordCharPredicate.INSTANCE.toString()); + } + + @Test + public void testCharacterParserStructuralEquality() { + assertTrue(CharacterParser.digit().isEqualTo(CharacterParser.digit())); + assertTrue(CharacterParser.letter().isEqualTo(CharacterParser.letter())); + assertTrue(CharacterParser.lowerCase().isEqualTo(CharacterParser.lowerCase())); + assertTrue(CharacterParser.upperCase().isEqualTo(CharacterParser.upperCase())); + assertTrue(CharacterParser.whitespace().isEqualTo(CharacterParser.whitespace())); + assertTrue(CharacterParser.word().isEqualTo(CharacterParser.word())); + assertTrue(CharacterParser.any().isEqualTo(CharacterParser.any())); + assertTrue(CharacterParser.none().isEqualTo(CharacterParser.none())); + assertTrue(CharacterParser.of('a').isEqualTo(CharacterParser.of('a'))); + assertTrue(CharacterParser.range('a', 'z').isEqualTo(CharacterParser.range('a', 'z'))); + assertTrue(CharacterParser.anyOf("abc").isEqualTo(CharacterParser.anyOf("abc"))); + assertTrue(CharacterParser.noneOf("abc").isEqualTo(CharacterParser.noneOf("abc"))); + assertTrue(CharacterParser.pattern("a-z").isEqualTo(CharacterParser.pattern("a-z"))); + assertTrue(CharacterParser.any(true).isEqualTo(CharacterParser.any(true))); + assertTrue(CharacterParser.of(0x1F680, true).isEqualTo(CharacterParser.of(0x1F680, true))); + assertTrue(CharacterParser.pattern("a-c", true).isEqualTo(CharacterParser.pattern("a-c", true))); + + assertFalse(CharacterParser.digit().isEqualTo(CharacterParser.letter())); + assertFalse(CharacterParser.of('a').isEqualTo(CharacterParser.of('b'))); + assertFalse(CharacterParser.range('a', 'z').isEqualTo(CharacterParser.range('a', 'y'))); + assertFalse(CharacterParser.any(false).isEqualTo(CharacterParser.any(true))); + assertFalse(CharacterParser.anyOf("abc").isEqualTo(CharacterParser.anyOf("abd"))); + assertFalse(CharacterParser.noneOf("abc").isEqualTo(CharacterParser.noneOf("abd"))); + assertFalse(CharacterParser.pattern("a-z").isEqualTo(CharacterParser.pattern("0-9"))); + } + + @Test + public void testOptimizedRangesFullRangeWithoutZero() { + CharacterPredicate predicate = CharacterPredicate.optimizedRanges( + List.of(new RangeCharPredicate(1, 65535)), false); + assertFalse(predicate.test(0)); + assertTrue(predicate.test(1)); + assertTrue(predicate.test(65535)); + assertFalse(predicate.isEqualTo(ConstantCharPredicate.any())); + } + + @Test + public void testOptimizedRangesDisjointSummingToFullCount() { + CharacterPredicate predicate = CharacterPredicate.optimizedRanges( + List.of(new RangeCharPredicate(1, 32768), + new RangeCharPredicate(32770, 65535)), false); + assertFalse(predicate.test(0)); + assertFalse(predicate.test(32769)); + assertTrue(predicate.test(1)); + assertTrue(predicate.test(32768)); + assertTrue(predicate.test(32770)); + assertFalse(predicate.isEqualTo(ConstantCharPredicate.any())); + } + + @Test + public void testOptimizedRangesUnicodeFullRangeWithoutZero() { + CharacterPredicate predicate = CharacterPredicate.optimizedRanges( + List.of(new RangeCharPredicate(1, 0x10ffff)), true); + assertFalse(predicate.test(0)); + assertTrue(predicate.test(1)); + assertTrue(predicate.test(0x10ffff)); + assertFalse(predicate.isEqualTo(ConstantCharPredicate.any())); + } + + @Test + public void testOptimizedRangesCoversEverything() { + CharacterPredicate predicate = CharacterPredicate.optimizedRanges( + List.of(new RangeCharPredicate(0, 0xffff)), false); + assertTrue(predicate.isEqualTo(ConstantCharPredicate.any())); + } + + @Test + public void testOptimizedRangesCoversEverythingUnicode() { + CharacterPredicate predicate = CharacterPredicate.optimizedRanges( + List.of(new RangeCharPredicate(0, 0x10ffff)), true); + assertTrue(predicate.isEqualTo(ConstantCharPredicate.any())); + } + + @Test + public void testOptimizedRangesSelectsLookupForDenseRanges() { + CharacterPredicate predicate = CharacterPredicate.optimizedRanges( + List.of( + new RangeCharPredicate(48, 57), + new RangeCharPredicate(65, 90), + new RangeCharPredicate(97, 122)), false); + assertTrue(predicate instanceof LookupCharPredicate); + assertTrue(predicate.test('a')); + assertTrue(predicate.test('Z')); + assertTrue(predicate.test('0')); + assertFalse(predicate.test('?')); + } + + @Test + public void testOptimizedRangesSelectsRangesForSparseRangesWithLargeSpan() { + CharacterPredicate predicate = CharacterPredicate.optimizedRanges( + List.of( + new RangeCharPredicate(97, 97), + new RangeCharPredicate(0x10000, 0x10000)), true); + assertTrue(predicate instanceof RangesCharPredicate); + assertTrue(predicate.test(97)); + assertTrue(predicate.test(0x10000)); + assertFalse(predicate.test(98)); + assertFalse(predicate.test(0)); + } + + @Test + public void testOptimizedRangesEmptyAndSingle() { + CharacterPredicate empty = CharacterPredicate.optimizedRanges(List.of()); + assertTrue(empty.isEqualTo(ConstantCharPredicate.none())); + + CharacterPredicate single = CharacterPredicate.optimizedRanges( + List.of(new RangeCharPredicate('a', 'a'))); + assertTrue(single instanceof SingleCharPredicate); + assertEquals('a', ((SingleCharPredicate) single).getValue()); + + CharacterPredicate range = CharacterPredicate.optimizedRanges( + List.of(new RangeCharPredicate('a', 'z'))); + assertTrue(range instanceof RangeCharPredicate); + } + + @Test + public void testOptimizedRangesOverlappingAndAdjacent() { + CharacterPredicate predicate = CharacterPredicate.optimizedRanges( + List.of( + new RangeCharPredicate('a', 'c'), + new RangeCharPredicate('c', 'e'), + new RangeCharPredicate('b', 'd'))); + assertTrue(predicate instanceof RangeCharPredicate); + assertEquals('a', ((RangeCharPredicate) predicate).getStart()); + assertEquals('e', ((RangeCharPredicate) predicate).getStop()); + } + + @Test + public void testOptimizedString() { + CharacterPredicate pred = CharacterPredicate.optimizedString("abc"); + assertTrue(pred.test('a')); + assertTrue(pred.test('b')); + assertTrue(pred.test('c')); + assertFalse(pred.test('d')); + + CharacterPredicate ignoreCase = CharacterPredicate.optimizedString("ab", true); + assertTrue(ignoreCase.test('a')); + assertTrue(ignoreCase.test('A')); + assertTrue(ignoreCase.test('b')); + assertTrue(ignoreCase.test('B')); + assertFalse(ignoreCase.test('c')); + + CharacterPredicate unicode = CharacterPredicate.optimizedString("a\uD83D\uDE80", false, true); + assertTrue(unicode.test('a')); + assertTrue(unicode.test(0x1F680)); + assertFalse(unicode.test('b')); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/primitive/CharacterPredicateTest.java b/petitparser-core/src/test/java/org/petitparser/parser/primitive/CharacterPredicateTest.java index a336166..8b06644 100644 --- a/petitparser-core/src/test/java/org/petitparser/parser/primitive/CharacterPredicateTest.java +++ b/petitparser-core/src/test/java/org/petitparser/parser/primitive/CharacterPredicateTest.java @@ -2,6 +2,8 @@ import org.junit.Test; +import java.util.List; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -210,4 +212,134 @@ public void testRange() { assertTrue(predicate.test('o')); assertFalse(predicate.test('p')); } + + @Test + public void testAnyOfOverloads() { + CharacterPredicate p1 = CharacterPredicate.anyOf("abc", true); + assertTrue(p1.test('a')); + assertFalse(p1.test('d')); + + CharacterPredicate p2 = CharacterPredicate.anyOf("aB", true, false); + assertTrue(p2.test('a')); + assertTrue(p2.test('A')); + assertTrue(p2.test('b')); + assertTrue(p2.test('B')); + assertFalse(p2.test('c')); + } + + @Test + public void testNoneOfOverloads() { + CharacterPredicate p1 = CharacterPredicate.noneOf("abc", true); + assertFalse(p1.test('a')); + assertTrue(p1.test('d')); + + CharacterPredicate p2 = CharacterPredicate.noneOf("aB", true, false); + assertFalse(p2.test('a')); + assertFalse(p2.test('A')); + assertFalse(p2.test('b')); + assertFalse(p2.test('B')); + assertTrue(p2.test('c')); + } + + @Test + public void testRangeOverloads() { + CharacterPredicate p1 = CharacterPredicate.range(0x61, 0x63); + assertTrue(p1.test('a')); + assertTrue(p1.test('b')); + assertTrue(p1.test('c')); + assertFalse(p1.test('d')); + } + + @Test + public void testRangesOverloads() { + CharacterPredicate p1 = CharacterPredicate.ranges(new char[]{'a', 'e'}, new char[]{'c', 'g'}); + assertTrue(p1.test('b')); + assertTrue(p1.test('f')); + assertFalse(p1.test('d')); + + CharacterPredicate p2 = CharacterPredicate.ranges(new int[]{0x61, 0x65}, new int[]{0x63, 0x67}); + assertTrue(p2.test('b')); + assertTrue(p2.test('f')); + assertFalse(p2.test('d')); + } + + @Test + public void testPatternOverloads() { + CharacterPredicate p1 = CharacterPredicate.pattern("a-c", true); + assertTrue(p1.test('a')); + assertTrue(p1.test('b')); + assertFalse(p1.test('d')); + + CharacterPredicate p2 = CharacterPredicate.pattern("a-c", true, false); + assertTrue(p2.test('a')); + assertTrue(p2.test('A')); + assertTrue(p2.test('b')); + assertTrue(p2.test('B')); + assertFalse(p2.test('d')); + } + + @Test + public void testOfOverloads() { + CharacterPredicate p1 = CharacterPredicate.of('a'); + assertTrue(p1.test('a')); + assertFalse(p1.test('b')); + + CharacterPredicate p2 = CharacterPredicate.of(0x1F680); + assertTrue(p2.test(0x1F680)); + assertFalse(p2.test('a')); + } + + @Test + public void testOptimizedStringAndRanges() { + CharacterPredicate p1 = CharacterPredicate.optimizedString("abc", true); + assertTrue(p1.test('a')); + assertTrue(p1.test('A')); + assertFalse(p1.test('d')); + + CharacterPredicate p2 = CharacterPredicate.optimizedRanges( + List.of(new RangeCharPredicate('a', 'c'), new RangeCharPredicate('e', 'g'))); + assertTrue(p2.test('b')); + assertTrue(p2.test('f')); + assertFalse(p2.test('d')); + } + + @Test + public void testSingletonPredicatesHashCode() { + assertTrue(CharacterPredicate.any().hashCode() != 0); + assertTrue(CharacterPredicate.none().hashCode() != 0); + assertTrue(DigitCharPredicate.INSTANCE.hashCode() != 0); + assertTrue(LetterCharPredicate.INSTANCE.hashCode() != 0); + assertTrue(LowercaseCharPredicate.INSTANCE.hashCode() != 0); + assertTrue(UppercaseCharPredicate.INSTANCE.hashCode() != 0); + assertTrue(WhitespaceCharPredicate.INSTANCE.hashCode() != 0); + assertTrue(WordCharPredicate.INSTANCE.hashCode() != 0); + } + + @Test + public void testDefaultInterfaceMethods() { + CharacterPredicate custom = (char c) -> c == 'z'; + assertTrue(custom.test((int) 'z')); + assertFalse(custom.test((int) 'a')); + assertFalse(custom.test(-1)); + assertFalse(custom.test(0x10000)); + assertTrue(custom.isEqualTo(custom)); + } + + @Test + public void testRangeCharPredicateOptimizedRangesDirect() { + CharacterPredicate p1 = RangeCharPredicate.optimizedRanges( + List.of(new RangeCharPredicate('0', '9'))); + assertTrue(p1.test('5')); + assertFalse(p1.test('a')); + + CharacterPredicate p2 = RangeCharPredicate.optimizedRanges( + List.of(new RangeCharPredicate('0', '9')), true); + assertTrue(p2.test('5')); + assertFalse(p2.test('a')); + } + + @Test(expected = IllegalArgumentException.class) + public void testLookupCharPredicateEmpty() { + new LookupCharPredicate(new boolean[0]); + } } diff --git a/petitparser-core/src/test/java/org/petitparser/parser/primitive/EpsilonParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/primitive/EpsilonParserTest.java new file mode 100644 index 0000000..a538ec8 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/primitive/EpsilonParserTest.java @@ -0,0 +1,97 @@ +package org.petitparser.parser.primitive; + +import org.junit.Test; +import org.petitparser.parser.Parser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; + +/** + * Tests for {@link EpsilonParser} and primitive factories. + */ +public class EpsilonParserTest { + + @Test + public void testEpsilonDefault() { + Parser parser = Parser.epsilon(); + assertSame(EpsilonParser.INSTANCE, parser); + assertNull(((EpsilonParser) parser).getValue()); + assertEquals("EpsilonParser", parser.toString()); + + assertSuccess(parser, "", null, 0); + assertSuccess(parser, "a", null, 0); + } + + @Test + public void testEpsilonWithValue() { + Parser parser = Parser.epsilon(42); + assertEquals(42, ((EpsilonParser) parser).getValue()); + assertEquals("EpsilonParser[42]", parser.toString()); + + assertSuccess(parser, "", 42, 0); + assertSuccess(parser, "a", 42, 0); + } + + @Test + public void testEpsilonFastParse() { + Parser parser = Parser.epsilon("val"); + assertEquals(0, parser.fastParseOn("abc", 0)); + assertEquals(2, parser.fastParseOn("abc", 2)); + } + + @Test + public void testEpsilonCopyAndEquality() { + Parser parser = Parser.epsilon(42); + Parser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + Parser differentValue = Parser.epsilon(43); + assertFalse(parser.isEqualTo(differentValue)); + + Parser nullValue = Parser.epsilon(); + assertFalse(parser.isEqualTo(nullValue)); + } + + @Test + public void testFailure() { + Parser parser = Parser.failure(); + assertEquals("unable to parse", ((FailureParser) parser).getMessage()); + assertFailure(parser, "", 0, "unable to parse"); + assertFailure(parser, "a", 0, "unable to parse"); + + Parser custom = Parser.failure("custom failure"); + assertEquals("custom failure", ((FailureParser) custom).getMessage()); + assertFailure(custom, "", 0, "custom failure"); + assertFailure(custom, "a", 0, "custom failure"); + + Parser copy = custom.copy(); + assertNotSame(custom, copy); + assertTrue(custom.isEqualTo(copy)); + assertTrue(copy.isEqualTo(custom)); + assertFalse(parser.isEqualTo(custom)); + + Parser withMessage = FailureParser.withMessage("custom failure"); + assertTrue(custom.isEqualTo(withMessage)); + + assertEquals(-1, parser.fastParseOn("abc", 0)); + } + + @Test + public void testConstant() { + Parser parser = CharacterParser.digit().plus().constant(42); + assertSuccess(parser, "123", 42, 3); + assertFailure(parser, "abc", 0, "digit expected"); + + assertEquals(3, parser.fastParseOn("123", 0)); + assertEquals(-1, parser.fastParseOn("abc", 0)); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/primitive/NewlineParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/primitive/NewlineParserTest.java new file mode 100644 index 0000000..0fd5a27 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/primitive/NewlineParserTest.java @@ -0,0 +1,78 @@ +package org.petitparser.parser.primitive; + +import org.junit.Test; +import org.petitparser.parser.Parser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; + +/** + * Tests for {@link NewlineParser}. + */ +public class NewlineParserTest { + + @Test + public void testDefault() { + NewlineParser parser = (NewlineParser) Parser.newline(); + assertSame(NewlineParser.INSTANCE, parser); + assertEquals("newline expected", parser.getMessage()); + assertTrue(parser.toString().contains("newline expected")); + + assertSuccess(parser, "\n", "\n", 1); + assertSuccess(parser, "\r\n", "\r\n", 2); + assertSuccess(parser, "\r", "\r", 1); + assertSuccess(parser, "\rx", "\r", 1); + assertSuccess(parser, "\r\nx", "\r\n", 2); + + assertFailure(parser, "", 0, "newline expected"); + assertFailure(parser, "\f", 0, "newline expected"); + } + + @Test + public void testCustomMessage() { + Parser parser = Parser.newline("line break expected"); + assertFailure(parser, "", 0, "line break expected"); + } + + @Test + public void testFastParse() { + Parser parser = Parser.newline(); + assertEquals(1, parser.fastParseOn("\n", 0)); + assertEquals(2, parser.fastParseOn("\r\n", 0)); + assertEquals(1, parser.fastParseOn("\r", 0)); + assertEquals(1, parser.fastParseOn("\rx", 0)); + assertEquals(2, parser.fastParseOn("\r\nx", 0)); + assertEquals(-1, parser.fastParseOn("a", 0)); + assertEquals(-1, parser.fastParseOn("", 0)); + } + + @Test + public void testNullCheck() { + try { + new NewlineParser(null); + fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testCopyAndEquality() { + Parser parser = Parser.newline(); + Parser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + Parser custom = Parser.newline("custom"); + assertFalse(parser.isEqualTo(custom)); + assertFalse(parser.isEqualTo(PositionParser.INSTANCE)); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/primitive/PositionParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/primitive/PositionParserTest.java new file mode 100644 index 0000000..ce525bd --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/primitive/PositionParserTest.java @@ -0,0 +1,53 @@ +package org.petitparser.parser.primitive; + +import org.junit.Test; +import org.petitparser.parser.Parser; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.petitparser.Assertions.assertSuccess; + +/** + * Tests for {@link PositionParser}. + */ +public class PositionParserTest { + + @Test + public void testPosition() { + Parser parser = Parser.position(); + assertSame(PositionParser.INSTANCE, parser); + assertSuccess(parser, "", 0, 0); + assertSuccess(parser, "a", 0, 0); + } + + @Test + public void testPositionInSequence() { + Parser parser = CharacterParser.of('a') + .seq(Parser.position()) + .seq(CharacterParser.of('b')); + assertSuccess(parser, "ab", Arrays.asList('a', 1, 'b'), 2); + } + + @Test + public void testFastParse() { + Parser parser = PositionParser.INSTANCE; + assertEquals(0, parser.fastParseOn("abc", 0)); + assertEquals(2, parser.fastParseOn("abc", 2)); + } + + @Test + public void testCopyAndEquality() { + Parser parser = PositionParser.INSTANCE; + Parser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + assertFalse(parser.isEqualTo(new EpsilonParser())); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/primitive/StringParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/primitive/StringParserTest.java new file mode 100644 index 0000000..8e2b789 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/primitive/StringParserTest.java @@ -0,0 +1,129 @@ +package org.petitparser.parser.primitive; + +import org.junit.Test; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests {@link StringParser} and {@link StringIgnoreCaseParser}. + */ +public class StringParserTest { + + @Test + public void testStringParserSuccess() { + Parser parser = StringParser.of("petit"); + Result result = parser.parse("petitparser"); + assertTrue(result.isSuccess()); + assertEquals("petit", result.get()); + assertEquals(5, result.getPosition()); + } + + @Test + public void testStringParserFailure() { + Parser parser = StringParser.of("petit"); + Result result = parser.parse("large"); + assertTrue(result.isFailure()); + assertEquals("petit expected", result.getMessage()); + assertEquals(0, result.getPosition()); + } + + @Test + public void testStringParserCustomMessage() { + StringParser parser = StringParser.of("petit", "expected petit"); + assertEquals("petit", parser.getValue()); + assertEquals("expected petit", parser.getMessage()); + Result result = parser.parse("large"); + assertTrue(result.isFailure()); + assertEquals("expected petit", result.getMessage()); + } + + @Test + public void testStringParserFastParse() { + Parser parser = StringParser.of("petit"); + assertEquals(5, parser.fastParseOn("petitparser", 0)); + assertEquals(8, parser.fastParseOn("abcpetitxyz", 3)); + assertEquals(-1, parser.fastParseOn("large", 0)); + assertEquals(-1, parser.fastParseOn("pet", 0)); + } + + @Test + public void testStringParserEquality() { + Parser p1 = StringParser.of("petit"); + Parser p2 = StringParser.of("petit"); + Parser p3 = StringParser.of("other"); + Parser p4 = StringParser.of("petit", "different message"); + + assertTrue(p1.isEqualTo(p2)); + assertFalse(p1.isEqualTo(p3)); + assertFalse(p1.isEqualTo(p4)); + assertEquals(p1.toString(), p2.toString()); + } + + @Test + public void testStringIgnoreCaseParserSuccess() { + Parser parser = StringParser.ofIgnoringCase("petit"); + Result r1 = parser.parse("petit"); + assertTrue(r1.isSuccess()); + assertEquals("petit", r1.get()); + assertEquals(5, r1.getPosition()); + + Result r2 = parser.parse("PETIT"); + assertTrue(r2.isSuccess()); + assertEquals("PETIT", r2.get()); + assertEquals(5, r2.getPosition()); + + Result r3 = parser.parse("PeTiT"); + assertTrue(r3.isSuccess()); + assertEquals("PeTiT", r3.get()); + assertEquals(5, r3.getPosition()); + } + + @Test + public void testStringIgnoreCaseParserFailure() { + Parser parser = StringParser.ofIgnoringCase("petit"); + Result result = parser.parse("large"); + assertTrue(result.isFailure()); + assertEquals("petit expected", result.getMessage()); + assertEquals(0, result.getPosition()); + } + + @Test + public void testStringIgnoreCaseParserFastParse() { + Parser parser = StringParser.ofIgnoringCase("petit"); + assertEquals(5, parser.fastParseOn("petitparser", 0)); + assertEquals(5, parser.fastParseOn("PETITPARSER", 0)); + assertEquals(8, parser.fastParseOn("abcPETITxyz", 3)); + assertEquals(-1, parser.fastParseOn("large", 0)); + assertEquals(-1, parser.fastParseOn("pet", 0)); + } + + @Test + public void testStringIgnoreCaseParserEquality() { + Parser p1 = StringIgnoreCaseParser.of("petit", "expected"); + Parser p2 = StringIgnoreCaseParser.of("PETIT", "expected"); + Parser p3 = StringIgnoreCaseParser.of("other", "expected"); + Parser p4 = StringParser.of("petit", "expected"); + + assertTrue(p1.isEqualTo(p2)); + assertFalse(p1.isEqualTo(p3)); + assertFalse(p1.isEqualTo(p4)); + assertEquals("petit", ((StringIgnoreCaseParser) p1).getValue()); + assertEquals("expected", ((StringIgnoreCaseParser) p1).getMessage()); + assertTrue(p1.toString().contains("expected")); + } + + @Test + public void testCopy() { + Parser p1 = StringParser.of("test"); + Parser c1 = p1.copy(); + assertTrue(p1.isEqualTo(c1)); + + Parser p2 = StringIgnoreCaseParser.of("test"); + Parser c2 = p2.copy(); + assertTrue(p2.isEqualTo(c2)); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/primitive/UnicodeCharacterParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/primitive/UnicodeCharacterParserTest.java new file mode 100644 index 0000000..5589519 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/primitive/UnicodeCharacterParserTest.java @@ -0,0 +1,262 @@ +package org.petitparser.parser.primitive; + +import org.junit.Test; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests {@link UnicodeCharacterParser} and Unicode code point support. + */ +public class UnicodeCharacterParserTest { + + private static final String ROCKET = "\uD83D\uDE80"; // U+1F680 + private static final String SMILEY = "\uD83D\uDE03"; // U+1F603 + private static final String GRIN = "\uD83D\uDE01"; // U+1F601 + private static final String GRINNING = "\uD83D\uDE00"; // U+1F600 + private static final String SWEAT_SMILE = "\uD83D\uDE05"; // U+1F605 + + @Test + public void testAnyUnicode() { + Parser parser = CharacterParser.any(true); + + Result r1 = parser.parse("a"); + assertTrue(r1.isSuccess()); + assertEquals("a", r1.get()); + assertEquals(1, r1.getPosition()); + + Result r2 = parser.parse(ROCKET); + assertTrue(r2.isSuccess()); + assertEquals(ROCKET, r2.get()); + assertEquals(2, r2.getPosition()); + + Result r3 = parser.parse(""); + assertTrue(r3.isFailure()); + assertEquals(0, r3.getPosition()); + + assertEquals(1, parser.fastParseOn("abc", 0)); + assertEquals(2, parser.fastParseOn(ROCKET + "abc", 0)); + assertEquals(-1, parser.fastParseOn("", 0)); + } + + @Test + public void testOfUnicode() { + Parser parser = CharacterParser.of(0x1F680, true); + + Result r1 = parser.parse(ROCKET); + assertTrue(r1.isSuccess()); + assertEquals(ROCKET, r1.get()); + assertEquals(2, r1.getPosition()); + + Result r2 = parser.parse(SMILEY); + assertTrue(r2.isFailure()); + + Result r3 = parser.parse("a"); + assertTrue(r3.isFailure()); + + assertEquals(2, parser.fastParseOn(ROCKET, 0)); + assertEquals(-1, parser.fastParseOn(SMILEY, 0)); + } + + @Test + public void testRangeUnicode() { + // U+1F601 (GRIN) to U+1F603 (SMILEY) + Parser parser = CharacterParser.range(0x1F601, 0x1F603, true); + + Result r1 = parser.parse(GRIN); + assertTrue(r1.isSuccess()); + assertEquals(GRIN, r1.get()); + + Result r2 = parser.parse(SMILEY); + assertTrue(r2.isSuccess()); + assertEquals(SMILEY, r2.get()); + + Result r3 = parser.parse(GRINNING); + assertTrue(r3.isFailure()); + + Result r4 = parser.parse(SWEAT_SMILE); + assertTrue(r4.isFailure()); + + assertEquals(2, parser.fastParseOn(GRIN, 0)); + assertEquals(-1, parser.fastParseOn(GRINNING, 0)); + } + + @Test + public void testAnyOfUnicode() { + Parser parser = CharacterParser.anyOf("x" + ROCKET + "z", true); + + Result r1 = parser.parse("x"); + assertTrue(r1.isSuccess()); + assertEquals("x", r1.get()); + + Result r2 = parser.parse(ROCKET); + assertTrue(r2.isSuccess()); + assertEquals(ROCKET, r2.get()); + + Result r3 = parser.parse("z"); + assertTrue(r3.isSuccess()); + assertEquals("z", r3.get()); + + Result r4 = parser.parse("y"); + assertTrue(r4.isFailure()); + } + + @Test + public void testNoneOfUnicode() { + Parser parser = CharacterParser.noneOf("x" + ROCKET, true); + + Result r1 = parser.parse("y"); + assertTrue(r1.isSuccess()); + assertEquals("y", r1.get()); + + Result r2 = parser.parse(SMILEY); + assertTrue(r2.isSuccess()); + assertEquals(SMILEY, r2.get()); + + Result r3 = parser.parse("x"); + assertTrue(r3.isFailure()); + + Result r4 = parser.parse(ROCKET); + assertTrue(r4.isFailure()); + } + + @Test + public void testPatternUnicodeSingle() { + Parser parser = CharacterParser.pattern("😮", true); + Result r1 = parser.parse("😮"); + assertTrue(r1.isSuccess()); + assertEquals("😮", r1.get()); + + Result r2 = parser.parse("😃"); + assertTrue(r2.isFailure()); + Result r3 = parser.parse("x"); + assertTrue(r3.isFailure()); + } + + @Test + public void testPatternUnicodeRange() { + // 😁 (0x1F601) to 😄 (0x1F604) + Parser parser = CharacterParser.pattern("😁-😄", true); + + Result r1 = parser.parse("😁"); + assertTrue(r1.isSuccess()); + Result r2 = parser.parse("😃"); + assertTrue(r2.isSuccess()); + Result r3 = parser.parse("😄"); + assertTrue(r3.isSuccess()); + + Result r4 = parser.parse("😀"); + assertTrue(r4.isFailure()); + Result r5 = parser.parse("😅"); + assertTrue(r5.isFailure()); + } + + @Test + public void testPatternUnicodeNegated() { + Parser parser = CharacterParser.pattern("^" + ROCKET, true); + + Result r1 = parser.parse(SMILEY); + assertTrue(r1.isSuccess()); + assertEquals(SMILEY, r1.get()); + + Result r2 = parser.parse("a"); + assertTrue(r2.isSuccess()); + assertEquals("a", r2.get()); + + Result r3 = parser.parse(ROCKET); + assertTrue(r3.isFailure()); + } + + @Test + public void testPatternUnicodeIgnoreCase() { + Parser parser = CharacterParser.pattern("а-в", null, true, true); + + assertTrue(parser.parse("а").isSuccess()); + assertTrue(parser.parse("б").isSuccess()); + assertTrue(parser.parse("в").isSuccess()); + assertTrue(parser.parse("А").isSuccess()); + assertTrue(parser.parse("Б").isSuccess()); + assertTrue(parser.parse("В").isSuccess()); + + assertFalse(parser.parse("г").isSuccess()); + assertFalse(parser.parse("Г").isSuccess()); + assertFalse(parser.parse("a").isSuccess()); + } + + @Test + public void testPatternUnicodeIgnoreCaseEdgeCases() { + // Eszett 'ß' upper-cases to "SS" (length > 1 code point) + Parser parser = CharacterParser.pattern("ß", null, true, true); + assertTrue(parser.parse("ß").isSuccess()); + assertFalse(parser.parse("S").isSuccess()); + + // Full Unicode range triggers the continue in expandCase + String fullRangeUnicode = "\u0000-" + new String(Character.toChars(0x10ffff)); + Parser parserFull = CharacterParser.pattern(fullRangeUnicode, null, true, true); + assertTrue(parserFull.parse("a").isSuccess()); + assertTrue(parserFull.parse(ROCKET).isSuccess()); + + // Full BMP range triggers continue in non-unicode expandCase + Parser parserBmp = CharacterParser.pattern("\u0000-\uffff", null, true, false); + assertTrue(parserBmp.parse("a").isSuccess()); + } + + @Test + public void testPatternAndRangeOverloads() { + Parser p1 = CharacterParser.pattern("a-z", "custom msg", true); + assertTrue(p1.parse("a").isSuccess()); + assertEquals("custom msg", p1.parse("1").getMessage()); + + Parser r1 = CharacterParser.range('a', 'z', true); + assertTrue(r1.parse("m").isSuccess()); + + Parser r2 = CharacterParser.range('a', 'z', "range msg", true); + assertTrue(r2.parse("m").isSuccess()); + assertEquals("range msg", r2.parse("1").getMessage()); + } + + @Test + public void testSurrogateEdgeCases() { + Parser parser = CharacterParser.any(true); + + // Lone high surrogate at end of buffer + Result r1 = parser.parse("\uD83D"); + assertTrue(r1.isSuccess()); + assertEquals("\uD83D", r1.get()); + assertEquals(1, r1.getPosition()); + + // High surrogate followed by non-low surrogate + Result r2 = parser.parse("\uD83Da"); + assertTrue(r2.isSuccess()); + assertEquals("\uD83D", r2.get()); + assertEquals(1, r2.getPosition()); + } + + @Test + public void testEqualityAndCopy() { + Parser p1 = CharacterParser.of(0x1F680, true); + Parser p2 = CharacterParser.of(0x1F680, true); + Parser p3 = CharacterParser.of(0x1F603, true); + Parser p4 = CharacterParser.of((char) 0x1F680, false); + + assertTrue(p1.isEqualTo(p2)); + assertFalse(p1.isEqualTo(p3)); + assertFalse(p1.isEqualTo(p4)); + + Parser copy = p1.copy(); + assertTrue(p1.isEqualTo(copy)); + + Parser neg = p1.neg("not expected"); + assertTrue(neg instanceof UnicodeCharacterParser); + assertTrue(neg.parse(SMILEY).isSuccess()); + assertFalse(neg.parse(ROCKET).isSuccess()); + + Parser negDefault = p1.neg(); + assertTrue(negDefault instanceof UnicodeCharacterParser); + assertTrue(negDefault.parse(SMILEY).isSuccess()); + assertFalse(negDefault.parse(ROCKET).isSuccess()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/repeating/RepeatingCharacterParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/repeating/RepeatingCharacterParserTest.java new file mode 100644 index 0000000..afa3b90 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/repeating/RepeatingCharacterParserTest.java @@ -0,0 +1,296 @@ +package org.petitparser.parser.repeating; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.StringParser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; + +/** + * Tests for {@link RepeatingCharacterParser}. + */ +public class RepeatingCharacterParserTest { + + @Test + public void testStarString() { + Parser parser = CharacterParser.digit().starString(); + assertTrue(parser instanceof RepeatingCharacterParser); + RepeatingCharacterParser rcp = (RepeatingCharacterParser) parser; + assertEquals(0, rcp.getMin()); + assertEquals(RepeatingParser.UNBOUNDED, rcp.getMax()); + assertEquals("digit expected", rcp.getMessage()); + assertSame(CharacterParser.digit().getMatcher().getClass(), rcp.getPredicate().getClass()); + + assertSuccess(parser, "", "", 0); + assertSuccess(parser, "a", "", 0); + assertSuccess(parser, "1", "1", 1); + assertSuccess(parser, "123", "123", 3); + assertSuccess(parser, "123a", "123", 3); + } + + @Test + public void testStarStringWithMessage() { + Parser parser = CharacterParser.digit().starString("custom message"); + assertTrue(parser instanceof RepeatingCharacterParser); + assertEquals("custom message", ((RepeatingCharacterParser) parser).getMessage()); + assertSuccess(parser, "123", "123", 3); + } + + @Test + public void testPlusString() { + Parser parser = CharacterParser.digit().plusString(); + assertTrue(parser instanceof RepeatingCharacterParser); + RepeatingCharacterParser rcp = (RepeatingCharacterParser) parser; + assertEquals(1, rcp.getMin()); + assertEquals(RepeatingParser.UNBOUNDED, rcp.getMax()); + + assertFailure(parser, "", 0, "digit expected"); + assertFailure(parser, "a", 0, "digit expected"); + assertSuccess(parser, "1", "1", 1); + assertSuccess(parser, "12", "12", 2); + assertSuccess(parser, "123b", "123", 3); + } + + @Test + public void testPlusStringWithMessage() { + Parser parser = CharacterParser.digit().plusString("digits expected"); + assertTrue(parser instanceof RepeatingCharacterParser); + assertFailure(parser, "", 0, "digits expected"); + assertFailure(parser, "a", 0, "digits expected"); + assertSuccess(parser, "1", "1", 1); + } + + @Test + public void testTimesString() { + Parser parser = CharacterParser.digit().timesString(3); + assertTrue(parser instanceof RepeatingCharacterParser); + RepeatingCharacterParser rcp = (RepeatingCharacterParser) parser; + assertEquals(3, rcp.getMin()); + assertEquals(3, rcp.getMax()); + + assertFailure(parser, "", 0, "digit expected"); + assertFailure(parser, "1", 1, "digit expected"); + assertFailure(parser, "12", 2, "digit expected"); + assertSuccess(parser, "123", "123", 3); + assertSuccess(parser, "1234", "123", 3); + } + + @Test + public void testTimesStringWithMessage() { + Parser parser = CharacterParser.digit().timesString(2, "two digits"); + assertTrue(parser instanceof RepeatingCharacterParser); + assertFailure(parser, "1", 1, "two digits"); + assertSuccess(parser, "12", "12", 2); + } + + @Test + public void testRepeatString() { + Parser parser = CharacterParser.digit().repeatString(2, 4); + assertTrue(parser instanceof RepeatingCharacterParser); + RepeatingCharacterParser rcp = (RepeatingCharacterParser) parser; + assertEquals(2, rcp.getMin()); + assertEquals(4, rcp.getMax()); + + assertFailure(parser, "", 0, "digit expected"); + assertFailure(parser, "1", 1, "digit expected"); + assertSuccess(parser, "12", "12", 2); + assertSuccess(parser, "123", "123", 3); + assertSuccess(parser, "1234", "1234", 4); + assertSuccess(parser, "12345", "1234", 4); + } + + @Test + public void testRepeatStringWithMessage() { + Parser parser = CharacterParser.digit().repeatString(2, 3, "expected 2-3 digits"); + assertTrue(parser instanceof RepeatingCharacterParser); + assertFailure(parser, "1", 1, "expected 2-3 digits"); + assertSuccess(parser, "12", "12", 2); + } + + @Test + public void testGeneralParserRepeatString() { + Parser parser = StringParser.of("ab").plusString(); + assertFalse(parser instanceof RepeatingCharacterParser); + + assertFailure(parser, "", 0); + assertFailure(parser, "a", 0); + assertSuccess(parser, "ab", "ab", 2); + assertSuccess(parser, "abab", "abab", 4); + assertSuccess(parser, "ababc", "abab", 4); + + Parser parserWithMsg = StringParser.of("ab").plusString("ab expected"); + assertFailure(parserWithMsg, "x", 0, "ab expected"); + + Parser starParser = StringParser.of("ab").starString(); + assertSuccess(starParser, "", "", 0); + assertSuccess(starParser, "ab", "ab", 2); + + Parser starParserWithMsg = StringParser.of("ab").starString("custom"); + assertSuccess(starParserWithMsg, "ab", "ab", 2); + + Parser timesParser = StringParser.of("ab").timesString(2); + assertFailure(timesParser, "ab", 2); + assertSuccess(timesParser, "abab", "abab", 4); + + Parser timesParserWithMsg = StringParser.of("ab").timesString(2, "two abs"); + assertFailure(timesParserWithMsg, "ab", 0, "two abs"); + } + + @Test + public void testFastParseOnOffset() { + Parser parser = CharacterParser.digit().repeatString(2, 4); + assertEquals(-1, parser.fastParseOn("a12b", 0)); + assertEquals(3, parser.fastParseOn("a12b", 1)); + assertEquals(5, parser.fastParseOn("a12345b", 1)); + } + + @Test + public void testParseOnOffset() { + Parser parser = CharacterParser.digit().repeatString(2, 4); + org.petitparser.context.Result r1 = parser.parseOn(new org.petitparser.context.Context("a12b", 1)); + assertTrue(r1.isSuccess()); + assertEquals("12", r1.get()); + assertEquals(3, r1.getPosition()); + + org.petitparser.context.Result r2 = parser.parseOn(new org.petitparser.context.Context("a12345b", 1)); + assertTrue(r2.isSuccess()); + assertEquals("1234", r2.get()); + assertEquals(5, r2.getPosition()); + + org.petitparser.context.Result r3 = parser.parseOn(new org.petitparser.context.Context("a1b", 1)); + assertTrue(r3.isFailure()); + assertEquals(2, r3.getPosition()); + assertEquals("digit expected", r3.getMessage()); + } + + @Test + public void testCopyAndEquality() { + RepeatingCharacterParser parser = CharacterParser.digit().plusString(); + RepeatingCharacterParser copy = parser.copy(); + + assertNotSame(parser, copy); + assertEquals(parser.getMin(), copy.getMin()); + assertEquals(parser.getMax(), copy.getMax()); + assertEquals(parser.getMessage(), copy.getMessage()); + assertSame(parser.getPredicate(), copy.getPredicate()); + + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + assertFalse(parser.isEqualTo(CharacterParser.digit().starString())); + assertFalse(parser.isEqualTo(CharacterParser.letter().plusString())); + assertFalse(parser.isEqualTo(CharacterParser.digit().plusString("other"))); + } + + @Test + public void testToString() { + Parser parser = CharacterParser.digit().plusString(); + assertEquals("RepeatingCharacterParser[digit expected, 1..*]", parser.toString()); + + Parser bounded = CharacterParser.digit().repeatString(2, 4); + assertEquals("RepeatingCharacterParser[digit expected, 2..4]", bounded.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNegativeMin() { + new RepeatingCharacterParser(Character::isDigit, "msg", -1, 5); + } + + @Test(expected = IllegalArgumentException.class) + public void testMinGreaterThanMax() { + new RepeatingCharacterParser(Character::isDigit, "msg", 5, 2); + } + + @Test(expected = NullPointerException.class) + public void testNullPredicate() { + new RepeatingCharacterParser(null, "msg", 1, 2); + } + + @Test(expected = NullPointerException.class) + public void testNullMessage() { + new RepeatingCharacterParser(Character::isDigit, null, 1, 2); + } + + @Test + public void testCharacterParserGetters() { + CharacterParser parser = CharacterParser.digit(); + assertEquals("digit expected", parser.getMessage()); + assertTrue(parser.getMatcher().test('5')); + assertFalse(parser.getMatcher().test('a')); + } + + @Test + public void testUnicodeSurrogateStarString() { + // Rocket emoji: \uD83D\uDE80 (code point 0x1F680) + Parser parser = CharacterParser.of(0x1F680, true).starString(); + assertTrue(parser instanceof RepeatingCharacterParser); + assertTrue(((RepeatingCharacterParser) parser).isUnicode()); + + assertSuccess(parser, "", "", 0); + assertSuccess(parser, "\uD83D\uDE80", "\uD83D\uDE80", 2); + assertSuccess(parser, "\uD83D\uDE80\uD83D\uDE80", "\uD83D\uDE80\uD83D\uDE80", 4); + assertSuccess(parser, "\uD83D\uDE80abc", "\uD83D\uDE80", 2); + assertSuccess(parser, "abc", "", 0); + + // Fast-parse verification + assertTrue(parser.accept("")); + assertTrue(parser.accept("\uD83D\uDE80\uD83D\uDE80")); + assertEquals(4, parser.fastParseOn("\uD83D\uDE80\uD83D\uDE80xyz", 0)); + assertEquals(0, parser.fastParseOn("xyz", 0)); + + // Isolated surrogate (not rocket) + assertSuccess(parser, "\uD83D", "", 0); + assertEquals(0, parser.fastParseOn("\uD83D", 0)); + } + + @Test + public void testUnicodeSurrogatePlusString() { + Parser parser = CharacterParser.of(0x1F680, true).plusString(); + assertTrue(parser instanceof RepeatingCharacterParser); + assertTrue(((RepeatingCharacterParser) parser).isUnicode()); + + assertFailure(parser, "", 0); + assertFailure(parser, "abc", 0); + assertFailure(parser, "\uD83D", 0); // incomplete surrogate + assertFailure(parser, "\uD83D\uDE00", 0); // grinning face (different surrogate pair) + + assertSuccess(parser, "\uD83D\uDE80", "\uD83D\uDE80", 2); + assertSuccess(parser, "\uD83D\uDE80\uD83D\uDE80\uD83D\uDE80", "\uD83D\uDE80\uD83D\uDE80\uD83D\uDE80", 6); + + // Fast-parse verification + assertFalse(parser.accept("")); + assertFalse(parser.accept("abc")); + assertFalse(parser.accept("\uD83D")); + assertTrue(parser.accept("\uD83D\uDE80")); + assertEquals(2, parser.fastParseOn("\uD83D\uDE80", 0)); + assertEquals(-1, parser.fastParseOn("xyz", 0)); + assertEquals(-1, parser.fastParseOn("\uD83D", 0)); + } + + @Test + public void testUnicodeSurrogateTimesString() { + Parser parser = CharacterParser.of(0x1F680, true).timesString(2); + assertTrue(parser instanceof RepeatingCharacterParser); + + assertFailure(parser, "", 0); + assertFailure(parser, "\uD83D\uDE80", 2); + assertFailure(parser, "\uD83D\uDE80\uD83D", 2); // incomplete second surrogate + + assertSuccess(parser, "\uD83D\uDE80\uD83D\uDE80", "\uD83D\uDE80\uD83D\uDE80", 4); + assertSuccess(parser, "\uD83D\uDE80\uD83D\uDE80\uD83D\uDE80", "\uD83D\uDE80\uD83D\uDE80", 4); + + // Fast parse verification + assertEquals(-1, parser.fastParseOn("", 0)); + assertEquals(-1, parser.fastParseOn("\uD83D\uDE80", 0)); + assertEquals(-1, parser.fastParseOn("\uD83D\uDE80\uD83D", 0)); + assertEquals(4, parser.fastParseOn("\uD83D\uDE80\uD83D\uDE80", 0)); + assertEquals(4, parser.fastParseOn("\uD83D\uDE80\uD83D\uDE80\uD83D\uDE80", 0)); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/repeating/SeparatedListTest.java b/petitparser-core/src/test/java/org/petitparser/parser/repeating/SeparatedListTest.java new file mode 100644 index 0000000..c701f8c --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/repeating/SeparatedListTest.java @@ -0,0 +1,228 @@ +package org.petitparser.parser.repeating; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link SeparatedList}. + */ +public class SeparatedListTest { + + @Test + public void testEmpty() { + SeparatedList list = new SeparatedList<>(); + assertTrue(list.getElements().isEmpty()); + assertTrue(list.getSeparators().isEmpty()); + assertTrue(list.getSequentialList().isEmpty()); + assertSame(list, list.getSequential()); + + Iterator iterator = list.iterator(); + assertFalse(iterator.hasNext()); + } + + @Test(expected = NoSuchElementException.class) + public void testIteratorExhausted() { + SeparatedList list = new SeparatedList<>(); + list.iterator().next(); + } + + @Test + public void testSingleElement() { + SeparatedList list = new SeparatedList<>(Collections.singletonList(42)); + assertEquals(Collections.singletonList(42), list.getElements()); + assertTrue(list.getSeparators().isEmpty()); + assertEquals(Collections.singletonList(42), list.getSequentialList()); + + List iterated = new ArrayList<>(); + for (Object item : list.getSequential()) { + iterated.add(item); + } + assertEquals(Collections.singletonList(42), iterated); + } + + @Test + public void testInterleaved() { + SeparatedList list = new SeparatedList<>( + Arrays.asList(1, 2, 3), + Arrays.asList("+", "*")); + assertEquals(Arrays.asList(1, 2, 3), list.getElements()); + assertEquals(Arrays.asList("+", "*"), list.getSeparators()); + + List expected = Arrays.asList(1, "+", 2, "*", 3); + assertEquals(expected, list.getSequentialList()); + + List iterated = new ArrayList<>(); + for (Object item : list) { + iterated.add(item); + } + assertEquals(expected, iterated); + } + + @Test + public void testImmutability() { + List elements = new ArrayList<>(Arrays.asList(1, 2)); + List separators = new ArrayList<>(Collections.singletonList("+")); + SeparatedList list = new SeparatedList<>(elements, separators); + + elements.add(3); + separators.add("*"); + assertEquals(2, list.getElements().size()); + assertEquals(1, list.getSeparators().size()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testElementsUnmodifiable() { + SeparatedList list = new SeparatedList<>(Collections.singletonList(1)); + list.getElements().add(2); + } + + @Test(expected = UnsupportedOperationException.class) + public void testSeparatorsUnmodifiable() { + SeparatedList list = new SeparatedList<>( + Arrays.asList(1, 2), Collections.singletonList("+")); + list.getSeparators().add("-"); + } + + @Test(expected = UnsupportedOperationException.class) + public void testSequentialListUnmodifiable() { + SeparatedList list = new SeparatedList<>(Collections.singletonList(1)); + list.getSequentialList().add(2); + } + + @Test(expected = NullPointerException.class) + public void testNullElements() { + new SeparatedList(null, Collections.emptyList()); + } + + @Test(expected = NullPointerException.class) + public void testNullSeparators() { + new SeparatedList(Collections.emptyList(), null); + } + + @Test + public void testFoldSingle() { + SeparatedList list = new SeparatedList<>(Collections.singletonList(42)); + assertEquals(Integer.valueOf(42), list.foldLeft((a, sep, b) -> a + b)); + assertEquals(Integer.valueOf(42), list.foldRight((a, sep, b) -> a + b)); + } + + @Test + public void testFoldLeft() { + // ((10 - 3) - 2) = 5 + SeparatedList list = new SeparatedList<>( + Arrays.asList(10, 3, 2), + Arrays.asList("-", "-")); + int result = list.foldLeft((a, sep, b) -> a - b); + assertEquals(5, result); + } + + @Test + public void testFoldRight() { + // (10 - (3 - 2)) = 9 + SeparatedList list = new SeparatedList<>( + Arrays.asList(10, 3, 2), + Arrays.asList("-", "-")); + int result = list.foldRight((a, sep, b) -> a - b); + assertEquals(9, result); + } + + @Test(expected = NoSuchElementException.class) + public void testFoldLeftEmptyThrows() { + new SeparatedList().foldLeft((a, sep, b) -> a + b); + } + + @Test(expected = NoSuchElementException.class) + public void testFoldRightEmptyThrows() { + new SeparatedList().foldRight((a, sep, b) -> a + b); + } + + @Test(expected = NullPointerException.class) + public void testFoldLeftNullCallback() { + new SeparatedList<>(Collections.singletonList(1)).foldLeft(null); + } + + @Test(expected = NullPointerException.class) + public void testFoldRightNullCallback() { + new SeparatedList<>(Collections.singletonList(1)).foldRight(null); + } + + @Test + public void testEqualsAndHashCode() { + SeparatedList list1 = new SeparatedList<>( + Arrays.asList(1, 2), Collections.singletonList(",")); + SeparatedList list2 = new SeparatedList<>( + Arrays.asList(1, 2), Collections.singletonList(",")); + SeparatedList diffElements = new SeparatedList<>( + Arrays.asList(1, 3), Collections.singletonList(",")); + SeparatedList diffSeparators = new SeparatedList<>( + Arrays.asList(1, 2), Collections.singletonList(";")); + + assertEquals(list1, list1); + assertEquals(list1, list2); + assertEquals(list2, list1); + assertEquals(list1.hashCode(), list2.hashCode()); + + assertNotEquals(list1, diffElements); + assertNotEquals(list1, diffSeparators); + assertNotEquals(list1, null); + assertNotEquals(list1, "other"); + } + + @Test(expected = IllegalArgumentException.class) + public void testInconsistentElementsAndSeparatorsTooFew() { + new SeparatedList<>(Arrays.asList(1, 2), Collections.emptyList()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInconsistentElementsAndSeparatorsTooMany() { + new SeparatedList<>(Collections.singletonList(1), Collections.singletonList("+")); + } + + @Test(expected = IllegalArgumentException.class) + public void testInconsistentEmptyWithSeparators() { + new SeparatedList<>(Collections.emptyList(), Collections.singletonList("+")); + } + + @Test(expected = IllegalArgumentException.class) + public void testSingleArgConstructorWithMultipleElementsThrows() { + new SeparatedList<>(Arrays.asList(1, 2)); + } + + @Test(expected = NoSuchElementException.class) + public void testIteratorExhaustedOnNonEmptyList() { + SeparatedList list = new SeparatedList<>(Collections.singletonList(1)); + Iterator iterator = list.iterator(); + assertEquals(1, iterator.next()); + iterator.next(); + } + + @Test + public void testExplicitFoldFunction() { + FoldFunction add = (left, sep, right) -> left + right; + SeparatedList list = new SeparatedList<>( + Arrays.asList(1, 2, 3), Arrays.asList("+", "+")); + assertEquals(Integer.valueOf(6), list.foldLeft(add)); + assertEquals(Integer.valueOf(6), list.foldRight(add)); + } + + @Test + public void testToString() { + assertEquals("SeparatedList()", new SeparatedList<>().toString()); + assertEquals("SeparatedList(1)", + new SeparatedList<>(Collections.singletonList(1)).toString()); + assertEquals("SeparatedList(1, +, 2, *, 3)", + new SeparatedList<>(Arrays.asList(1, 2, 3), Arrays.asList("+", "*")).toString()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/parser/repeating/SeparatedRepeatingParserTest.java b/petitparser-core/src/test/java/org/petitparser/parser/repeating/SeparatedRepeatingParserTest.java new file mode 100644 index 0000000..22e20ab --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/parser/repeating/SeparatedRepeatingParserTest.java @@ -0,0 +1,201 @@ +package org.petitparser.parser.repeating; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.SequentialParser; +import org.petitparser.parser.primitive.CharacterParser; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; + +/** + * Tests for {@link SeparatedRepeatingParser}. + */ +public class SeparatedRepeatingParserTest { + + private final Parser element = CharacterParser.of('a'); + private final Parser separator = CharacterParser.of(','); + + @Test + public void testStarSeparated() { + Parser parser = element.starSeparated(separator); + assertTrue(parser instanceof SeparatedRepeatingParser); + assertTrue(parser instanceof SequentialParser); + + assertSuccess(parser, "", new SeparatedList(), 0); + assertSuccess(parser, "b", new SeparatedList(), 0); + assertSuccess(parser, "a", new SeparatedList<>(Collections.singletonList('a')), 1); + assertSuccess(parser, "a,a", new SeparatedList<>( + Arrays.asList('a', 'a'), Collections.singletonList(',')), 3); + assertSuccess(parser, "a,a,a", new SeparatedList<>( + Arrays.asList('a', 'a', 'a'), Arrays.asList(',', ',')), 5); + assertSuccess(parser, "a,b", new SeparatedList<>(Collections.singletonList('a')), 1); + assertSuccess(parser, "a,", new SeparatedList<>(Collections.singletonList('a')), 1); + } + + @Test + public void testPlusSeparated() { + Parser parser = element.plusSeparated(separator); + assertTrue(parser instanceof SeparatedRepeatingParser); + + assertFailure(parser, "", 0, "'a' expected"); + assertFailure(parser, "b", 0, "'a' expected"); + assertSuccess(parser, "a", new SeparatedList<>(Collections.singletonList('a')), 1); + assertSuccess(parser, "a,a", new SeparatedList<>( + Arrays.asList('a', 'a'), Collections.singletonList(',')), 3); + assertSuccess(parser, "a,a,a", new SeparatedList<>( + Arrays.asList('a', 'a', 'a'), Arrays.asList(',', ',')), 5); + assertSuccess(parser, "a,b", new SeparatedList<>(Collections.singletonList('a')), 1); + assertSuccess(parser, "a,", new SeparatedList<>(Collections.singletonList('a')), 1); + } + + @Test + public void testTimesSeparated() { + Parser parser = element.timesSeparated(separator, 2); + assertTrue(parser instanceof SeparatedRepeatingParser); + + assertFailure(parser, "", 0, "'a' expected"); + assertFailure(parser, "a", 1, "',' expected"); + assertFailure(parser, "a,", 2, "'a' expected"); + assertSuccess(parser, "a,a", new SeparatedList<>( + Arrays.asList('a', 'a'), Collections.singletonList(',')), 3); + assertSuccess(parser, "a,a,a", new SeparatedList<>( + Arrays.asList('a', 'a'), Collections.singletonList(',')), 3); + } + + @Test + public void testRepeatSeparated() { + Parser parser = element.repeatSeparated(separator, 2, 3); + assertTrue(parser instanceof SeparatedRepeatingParser); + + assertFailure(parser, "", 0, "'a' expected"); + assertFailure(parser, "a", 1, "',' expected"); + assertSuccess(parser, "a,a", new SeparatedList<>( + Arrays.asList('a', 'a'), Collections.singletonList(',')), 3); + assertSuccess(parser, "a,a,a", new SeparatedList<>( + Arrays.asList('a', 'a', 'a'), Arrays.asList(',', ',')), 5); + assertSuccess(parser, "a,a,a,a", new SeparatedList<>( + Arrays.asList('a', 'a', 'a'), Arrays.asList(',', ',')), 5); + } + + @Test + public void testFastParse() { + Parser parser = element.plusSeparated(separator); + assertEquals(-1, parser.fastParseOn("", 0)); + assertEquals(-1, parser.fastParseOn("b", 0)); + assertEquals(1, parser.fastParseOn("a", 0)); + assertEquals(3, parser.fastParseOn("a,a", 0)); + assertEquals(5, parser.fastParseOn("a,a,a", 0)); + assertEquals(1, parser.fastParseOn("a,b", 0)); + assertEquals(1, parser.fastParseOn("a,", 0)); + + Parser bounded = element.repeatSeparated(separator, 2, 3); + assertEquals(-1, bounded.fastParseOn("a", 0)); + assertEquals(3, bounded.fastParseOn("a,a", 0)); + assertEquals(5, bounded.fastParseOn("a,a,a", 0)); + assertEquals(5, bounded.fastParseOn("a,a,a,a", 0)); + } + + @Test + public void testFastParseOffset() { + Parser parser = element.plusSeparated(separator); + assertEquals(-1, parser.fastParseOn("xxa,a", 0)); + assertEquals(5, parser.fastParseOn("xxa,a", 2)); + assertEquals(5, parser.fastParseOn("xxa,a,b", 2)); + } + + @Test + public void testParseOnOffset() { + Parser parser = element.plusSeparated(separator); + org.petitparser.context.Result result = parser.parseOn( + new org.petitparser.context.Context("xxa,a", 2)); + assertTrue(result.isSuccess()); + assertEquals(5, result.getPosition()); + assertEquals(new SeparatedList( + Arrays.asList('a', 'a'), Collections.singletonList(',')), result.get()); + } + + @Test + public void testToString() { + SeparatedRepeatingParser parser = element.plusSeparated(separator); + assertEquals("SeparatedRepeatingParser[1..*]", parser.toString()); + + SeparatedRepeatingParser bounded = element.repeatSeparated(separator, 2, 4); + assertEquals("SeparatedRepeatingParser[2..4]", bounded.toString()); + } + + @Test + public void testChildrenAndReplace() { + SeparatedRepeatingParser parser = element.plusSeparated(separator); + assertEquals(Arrays.asList(element, separator), parser.getChildren()); + + Parser newElement = CharacterParser.of('b'); + Parser newSeparator = CharacterParser.of(';'); + + parser.replace(element, newElement); + assertEquals(newElement, parser.getChildren().get(0)); + + parser.replace(separator, newSeparator); + assertSame(newSeparator, parser.getSeparator()); + + parser.setSeparator(separator); + assertSame(separator, parser.getSeparator()); + } + + @Test + public void testCopyAndEquality() { + SeparatedRepeatingParser parser = element.plusSeparated(separator); + SeparatedRepeatingParser copy = parser.copy(); + + assertNotSame(parser, copy); + assertTrue(parser.isEqualTo(copy)); + assertTrue(copy.isEqualTo(parser)); + + assertFalse(parser.isEqualTo(element.starSeparated(separator))); + assertFalse(parser.isEqualTo(element.plusSeparated(CharacterParser.of(';')))); + assertFalse(parser.isEqualTo(CharacterParser.of('b').plusSeparated(separator))); + } + + @Test(expected = NullPointerException.class) + public void testNullDelegate() { + new SeparatedRepeatingParser(null, separator, 1, 2); + } + + @Test(expected = NullPointerException.class) + public void testNullSeparator() { + new SeparatedRepeatingParser(element, null, 1, 2); + } + + @Test(expected = NullPointerException.class) + public void testSetNullSeparator() { + SeparatedRepeatingParser parser = element.plusSeparated(separator); + parser.setSeparator(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNegativeMin() { + new SeparatedRepeatingParser(element, separator, -1, 2); + } + + @Test(expected = IllegalArgumentException.class) + public void testMinGreaterThanMax() { + new SeparatedRepeatingParser(element, separator, 5, 2); + } + + @Test + public void testBackwardCompatibilitySeparatedBy() { + Parser parser = element.separatedBy(separator); + assertSuccess(parser, "a,a,a", Arrays.asList('a', ',', 'a', ',', 'a'), 5); + + Parser delimited = element.delimitedBy(separator); + assertSuccess(delimited, "a,a,", Arrays.asList('a', ',', 'a', ','), 4); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/tools/ExpressionBuilderTest.java b/petitparser-core/src/test/java/org/petitparser/tools/ExpressionBuilderTest.java index 331e3d8..04b1843 100644 --- a/petitparser-core/src/test/java/org/petitparser/tools/ExpressionBuilderTest.java +++ b/petitparser-core/src/test/java/org/petitparser/tools/ExpressionBuilderTest.java @@ -2,13 +2,24 @@ import org.junit.Before; import org.junit.Test; +import org.petitparser.context.Result; import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.EpsilonParser; +import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.petitparser.utils.functions.Function3; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.noneOf; import static org.petitparser.parser.primitive.CharacterParser.of; import static org.petitparser.parser.primitive.StringParser.of; @@ -20,6 +31,7 @@ public class ExpressionBuilderTest { Parser parser; @Before + @SuppressWarnings("rawtypes") public void setUpParser() { ExpressionBuilder builder = new ExpressionBuilder(); builder.group() @@ -53,7 +65,7 @@ private void assertParse(String input, Object expected) { @Before public void setUpEvaluator() { - ExpressionBuilder builder = new ExpressionBuilder(); + ExpressionBuilder builder = new ExpressionBuilder<>(); builder.group() .primitive(digit().plus().seq(of('.') .seq(digit().plus()).optional()) @@ -313,4 +325,335 @@ public void testEvaluatePrefixNegate() { assertEvaluation("--1", 1); assertEvaluation("---1", -1); } + + @Test + public void testBuildWithoutGroupsFails() { + Parser parser = new ExpressionBuilder<>().build(); + assertNotNull(parser); + Result result = parser.parse(""); + assertTrue(result.isFailure()); + } + + @Test + public void testBuilderLevelPrimitive() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(digit().map(ch -> Character.digit((char) ch, 10))); + Parser parser = builder.build().end(); + assertEquals(Integer.valueOf(5), parser.parse("5").get()); + assertTrue(parser.parse("a").isFailure()); + } + + @Test + public void testBuilderLevelPrimitiveWithAction() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(digit().plus().flatten(), (String s) -> Integer.parseInt(s)); + Parser parser = builder.build().end(); + assertEquals(Integer.valueOf(123), parser.parse("123").get()); + } + + @Test + public void testMultipleBuilderPrimitives() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(of('a').map(Object::toString)); + builder.primitive(of('b').map(Object::toString)); + Parser parser = builder.build().end(); + assertEquals("a", parser.parse("a").get()); + assertEquals("b", parser.parse("b").get()); + assertTrue(parser.parse("c").isFailure()); + } + + @Test + public void testEmptyBuildWithOnlyPrimitives() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(of('a').map(Object::toString)); + Parser parser = builder.build().end(); + assertEquals("a", parser.parse("a").get()); + } + + @Test + public void testBuilderLoopback() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(of('a').seq(builder.loopback()).flatten()); + builder.primitive(of('b').map(Object::toString)); + Parser parser = builder.build().end(); + assertEquals("b", parser.parse("b").get()); + assertEquals("ab", parser.parse("ab").get()); + assertEquals("aab", parser.parse("aab").get()); + assertEquals("aaab", parser.parse("aaab").get()); + assertTrue(parser.parse("a").isFailure()); + } + + @Test + public void testBuilderGetLoopbackGetter() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + assertNotNull(builder.getLoopback()); + assertSame(builder.getLoopback(), builder.loopback()); + } + + @Test + public void testOptionalGroupBasic() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(digit().map(Object::toString)); + builder.group() + .wrapper(of('('), of(')'), (l, v, r) -> "(" + v + ")") + .optional("∅"); + Parser parser = builder.build().end(); + assertEquals("∅", parser.parse("").get()); + assertEquals("(∅)", parser.parse("()").get()); + assertEquals("1", parser.parse("1").get()); + assertEquals("(1)", parser.parse("(1)").get()); + } + + @Test + public void testOptionalGroupNull() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(digit().map(Object::toString)); + builder.group().optional(null); + Parser parser = builder.build().end(); + assertNull(parser.parse("").get()); + assertEquals("1", parser.parse("1").get()); + } + + @Test(expected = IllegalStateException.class) + public void testOptionalGroupRepeatedFails() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + ExpressionBuilder.ExpressionGroup group = builder.group(); + group.optional("foo"); + group.optional("bar"); + } + + @Test + public void testOptionalWithRegexGrammar() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(noneOf("()!|&?").map(Object::toString)); + builder.group() + .wrapper(of('('), of(')'), (l, v, r) -> "(" + v + ")") + .prefix(of('!'), (op, v) -> "!(" + v + ")") + .postfix(of('?'), (v, op) -> "(" + v + ")?") + .left(of('|'), (l, op, r) -> "(" + l + "|" + r + ")") + .right(of('&'), (l, op, r) -> "(" + l + "&" + r + ")"); + builder.group() + .left(new EpsilonParser(null), (l, op, r) -> "[" + l + r + "]") + .optional("∅"); + Parser parser = builder.build().end(); + + assertEquals("∅", parser.parse("").get()); + assertEquals("a", parser.parse("a").get()); + assertEquals("[ab]", parser.parse("ab").get()); + assertEquals("[[ab]c]", parser.parse("abc").get()); + assertEquals("(a&b)", parser.parse("a&b").get()); + assertEquals("(a&(b&c))", parser.parse("a&b&c").get()); + assertEquals("(a|b)", parser.parse("a|b").get()); + assertEquals("((a|b)|c)", parser.parse("a|b|c").get()); + assertEquals("(a)?", parser.parse("a?").get()); + assertEquals("((a)?)?", parser.parse("a??").get()); + assertEquals("!(a)", parser.parse("!a").get()); + assertEquals("!(!(a))", parser.parse("!!a").get()); + assertEquals("(∅)", parser.parse("()").get()); + assertEquals("(a)", parser.parse("(a)").get()); + assertEquals("([ab])", parser.parse("(ab)").get()); + } + + @Test + public void testTypedEvaluator() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(digit().plus().seq(of('.') + .seq(digit().plus()).optional()) + .flatten() + .trim(), Double::parseDouble); + builder.group() + .wrapper( + of('(').trim(), + of(')').trim(), + (l, v, r) -> v); + builder.group() + .prefix(of('-').trim(), (op, v) -> -v); + builder.group() + .postfix(of("++").trim(), (v, op) -> v + 1.0) + .postfix(of("--").trim(), (v, op) -> v - 1.0); + builder.group() + .right(of('^').trim(), + (l, op, r) -> Math.pow(l, r)); + builder.group() + .left(of('*').trim(), + (l, op, r) -> l * r) + .left(of('/').trim(), + (l, op, r) -> l / r); + builder.group() + .left(of('+').trim(), + (l, op, r) -> l + r) + .left(of('-').trim(), + (l, op, r) -> l - r); + Parser typedEvaluator = builder.build().end(); + + assertEquals(0.0, (Double) typedEvaluator.parse("0").get(), 1e-5); + assertEquals(1.2, (Double) typedEvaluator.parse("1.2").get(), 1e-5); + assertEquals(-1.2, (Double) typedEvaluator.parse("-1.2").get(), 1e-5); + assertEquals(3.0, (Double) typedEvaluator.parse("1 + 2").get(), 1e-5); + assertEquals(6.0, (Double) typedEvaluator.parse("1 + 2 + 3").get(), 1e-5); + assertEquals(-4.0, (Double) typedEvaluator.parse("1 - 2 - 3").get(), 1e-5); + assertEquals(6.0, (Double) typedEvaluator.parse("2 * 3").get(), 1e-5); + assertEquals(4.0, (Double) typedEvaluator.parse("12 / 3").get(), 1e-5); + assertEquals(25.0, (Double) typedEvaluator.parse("100 / 2 / 2").get(), 1e-5); + assertEquals(8.0, (Double) typedEvaluator.parse("2 ^ 3").get(), 1e-5); + assertEquals(262144.0, (Double) typedEvaluator.parse("4 ^ 3 ^ 2").get(), 1e-5); + assertEquals(14.0, (Double) typedEvaluator.parse("2 * (3 + 4)").get(), 1e-5); + assertEquals(10.0, (Double) typedEvaluator.parse("2 * 3 + 4").get(), 1e-5); + assertEquals(14.0, (Double) typedEvaluator.parse("2 + 3 * 4").get(), 1e-5); + assertEquals(1.0, (Double) typedEvaluator.parse("0++").get(), 1e-5); + assertEquals(2.0, (Double) typedEvaluator.parse("0++++").get(), 1e-5); + assertEquals(0.0, (Double) typedEvaluator.parse("1--").get(), 1e-5); + assertEquals(1.0, (Double) typedEvaluator.parse("--1").get(), 1e-5); + } + + @Test + public void testFastParseOn() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(digit().plus().flatten(), Double::parseDouble); + builder.group().left(of('+'), (l, op, r) -> l + r); + Parser parser = builder.build().end(); + assertEquals(3, parser.fastParseOn("1+2", 0)); + assertEquals(-1, parser.fastParseOn("1+", 0)); + } + + @Test + public void testBuilderAndGroupPrimitivesCombined() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(of('a').map(ch -> 1)); + builder.group().primitive(of('b').map(ch -> 2)); + Parser parser = builder.build().end(); + assertEquals(Integer.valueOf(1), parser.parse("a").get()); + assertEquals(Integer.valueOf(2), parser.parse("b").get()); + } + + @Test + public void testConstructors() { + assertNotNull(new ExpressionBuilder.ExpressionGroup<>()); + assertNotNull(new ExpressionBuilder.ExpressionGroupBase()); + } + + @Test + public void testBuildChoice() { + Parser otherwise = of('x'); + assertSame(otherwise, ExpressionBuilder.buildChoice(Collections.emptyList(), otherwise)); + assertNull(ExpressionBuilder.buildChoice(Collections.emptyList())); + } + + @Test + public void testGroupPrimitiveWithNullAction() { + ExpressionBuilder.ExpressionGroupBase group = new ExpressionBuilder.ExpressionGroupBase(); + group.primitive(of('x'), null); + Parser parser = group.build(null); + assertEquals('x', (char) parser.parse("x").get()); + } + + @Test + public void testGroupWrapperWithNullAction() { + ExpressionBuilder builder = new ExpressionBuilder<>(); + builder.primitive(of('x').map(Object::toString)); + builder.group().wrapper(of('['), of(']'), (Function, String>) null); + Parser parser = builder.build().end(); + assertEquals(asList('[', "x", ']'), parser.parse("[x]").get()); + } + + @Test + public void testGroupBuildNullInner() { + ExpressionBuilder.ExpressionGroupBase group = new ExpressionBuilder.ExpressionGroupBase(); + assertNull(group.build(null)); + } + + @Test(expected = NullPointerException.class) + public void testBuilderPrimitiveNullParser() { + new ExpressionBuilder<>().primitive(null); + } + + @Test(expected = NullPointerException.class) + public void testGroupPrefixNullParser() { + new ExpressionBuilder<>().group().prefix(null); + } + + @Test(expected = NullPointerException.class) + public void testGroupPrefixNullParserWithAction() { + new ExpressionBuilder<>().group().prefix(null, (op, v) -> v); + } + + @Test(expected = NullPointerException.class) + public void testGroupPrefixNullAction() { + new ExpressionBuilder().group().prefix(of('-'), (BiFunction) null); + } + + @Test(expected = NullPointerException.class) + public void testGroupPostfixNullParser() { + new ExpressionBuilder<>().group().postfix(null); + } + + @Test(expected = NullPointerException.class) + public void testGroupPostfixNullParserWithAction() { + new ExpressionBuilder<>().group().postfix(null, (v, op) -> v); + } + + @Test(expected = NullPointerException.class) + public void testGroupPostfixNullAction() { + new ExpressionBuilder().group().postfix(of('+'), (BiFunction) null); + } + + @Test(expected = NullPointerException.class) + public void testGroupRightNullParser() { + new ExpressionBuilder<>().group().right(null); + } + + @Test(expected = NullPointerException.class) + public void testGroupRightNullParserWithAction() { + new ExpressionBuilder<>().group().right(null, (l, op, r) -> l); + } + + @Test(expected = NullPointerException.class) + public void testGroupRightNullAction() { + new ExpressionBuilder().group().right(of('^'), (Function3) null); + } + + @Test(expected = NullPointerException.class) + public void testGroupLeftNullParser() { + new ExpressionBuilder<>().group().left(null); + } + + @Test(expected = NullPointerException.class) + public void testGroupLeftNullParserWithAction() { + new ExpressionBuilder<>().group().left(null, (l, op, r) -> l); + } + + @Test(expected = NullPointerException.class) + public void testGroupLeftNullAction() { + new ExpressionBuilder().group().left(of('+'), (Function3) null); + } + + @Test(expected = NullPointerException.class) + public void testGroupWrapperNullLeft() { + new ExpressionBuilder<>().group().wrapper(null, of(')')); + } + + @Test(expected = NullPointerException.class) + public void testGroupWrapperNullRight() { + new ExpressionBuilder<>().group().wrapper(of('('), null); + } + + @Test(expected = NullPointerException.class) + public void testGroupWrapperNullLeftWithAction() { + new ExpressionBuilder<>().group().wrapper(null, of(')'), (l, v, r) -> v); + } + + @Test(expected = NullPointerException.class) + public void testGroupWrapperNullRightWithAction() { + new ExpressionBuilder<>().group().wrapper(of('('), null, (l, v, r) -> v); + } + + @Test(expected = NullPointerException.class) + public void testGroupWrapperNullAction() { + new ExpressionBuilder().group().wrapper(of('('), of(')'), (Function3) null); + } + + @Test(expected = NullPointerException.class) + public void testGroupBaseNullLoopback() { + new ExpressionBuilder.ExpressionGroupBase(null); + } } diff --git a/petitparser-core/src/test/java/org/petitparser/tools/GrammarDefinitionTest.java b/petitparser-core/src/test/java/org/petitparser/tools/GrammarDefinitionTest.java index c117f57..b22837f 100644 --- a/petitparser-core/src/test/java/org/petitparser/tools/GrammarDefinitionTest.java +++ b/petitparser-core/src/test/java/org/petitparser/tools/GrammarDefinitionTest.java @@ -185,7 +185,7 @@ public void testReferenceCopy() { } @Test - @SuppressWarnings("EqualsBetweenInconvertibleTypes") + @SuppressWarnings({ "EqualsBetweenInconvertibleTypes", "unlikely-arg-type" }) public void testReferenceEquals() { Parser reference = buggedDefinition.ref("start"); assertFalse(Objects.equals(reference, null)); @@ -241,4 +241,4 @@ public void testExpressionGrammar() { assertTrue(parser.accept("1 + (2 * 3)")); assertTrue(parser.accept("(1 + 2) * 3")); } -} \ No newline at end of file +} diff --git a/petitparser-core/src/test/java/org/petitparser/tools/IndentTest.java b/petitparser-core/src/test/java/org/petitparser/tools/IndentTest.java new file mode 100644 index 0000000..1a44280 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/tools/IndentTest.java @@ -0,0 +1,561 @@ +package org.petitparser.tools; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.StringParser; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.petitparser.Assertions.assertFailure; +import static org.petitparser.Assertions.assertSuccess; + +/** + * Tests for {@link Indent}. + */ +public class IndentTest { + + static class IndentList extends GrammarDefinition { + final Indent indent = new Indent(); + + IndentList() { + def("start", Parser.seq( + ref("newlines").optional(), + ref("things").optional(), + ref("newlines").optional(), + new org.petitparser.parser.combinators.EndOfInputParser("end of input expected")) + .map((nl1, things, nl2, end) -> things != null ? things : Collections.emptyList())); + + def("things", Parser.seq( + indent.same(), + ref("object").or(ref("line"))) + .map((same, value) -> value) + .plus()); + + def("object", Parser.seq( + ref("key"), + ref("block").or(ref("inline"))) + .map((key, values) -> { + Map map = new HashMap<>(); + map.put((String) key, values); + return map; + })); + + def("key", Parser.seq( + CharacterParser.pattern("^ \t\r\n:").plusString(), + indent.getParser().star(), + CharacterParser.of(':'), + indent.getParser().star()) + .map((key, ws1, colon, ws2) -> key)); + + def("block", Parser.seq( + ref("newlines"), + indent.during(ref("things"))) + .map((newlines, things) -> things)); + + def("inline", ref("line")); + + def("line", Parser.seq( + Parser.newline().neg().plus().flatten(), + ref("newlines").optional()) + .map((line, newlines) -> line)); + + def("whitespaces", indent.getParser().star()); + + def("newline", Parser.newline()); + + def("newlines", Parser.seq(ref("whitespaces"), ref("newline")).plus()); + } + } + + private static Map mapOf(String key, Object value) { + Map map = new HashMap<>(); + map.put(key, value); + return map; + } + + @Test + public void testDefinitionEmpty() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + assertSuccess(parser, "", Collections.emptyList()); + assertSuccess(parser, "\n", Collections.emptyList()); + assertSuccess(parser, "\n\r", Collections.emptyList()); + assertSuccess(parser, "\r", Collections.emptyList()); + assertSuccess(parser, "\n\n", Collections.emptyList()); + assertSuccess(parser, "\n\r\n\r", Collections.emptyList()); + assertSuccess(parser, "\r\r", Collections.emptyList()); + + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionNewlineBefore() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + assertSuccess(parser, "\na", Collections.singletonList("a")); + assertSuccess(parser, "\n\ra", Collections.singletonList("a")); + assertSuccess(parser, "\ra", Collections.singletonList("a")); + assertSuccess(parser, "\n\na", Collections.singletonList("a")); + assertSuccess(parser, "\n\r\n\ra", Collections.singletonList("a")); + assertSuccess(parser, "\r\ra", Collections.singletonList("a")); + + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionNewlineAfter() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + assertSuccess(parser, "a\n", Collections.singletonList("a")); + assertSuccess(parser, "a\n\r", Collections.singletonList("a")); + assertSuccess(parser, "a\r", Collections.singletonList("a")); + assertSuccess(parser, "a\n\n", Collections.singletonList("a")); + assertSuccess(parser, "a\n\r\n\r", Collections.singletonList("a")); + assertSuccess(parser, "a\r\r", Collections.singletonList("a")); + + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionSingleIndent() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + List expected = Collections.singletonList(mapOf("a", Collections.singletonList("b"))); + assertSuccess(parser, "a:\n b", expected); + assertSuccess(parser, "a:\n\tb", expected); + assertSuccess(parser, "a:\n \tb", expected); + assertSuccess(parser, "a:\n\t b", expected); + + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionSameIndent() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + List expected = Collections.singletonList(mapOf("a", Arrays.asList("b", "c"))); + assertSuccess(parser, "a:\n b\n c", expected); + assertSuccess(parser, "a:\n\tb\n\tc", expected); + assertSuccess(parser, "a:\n \tb\n \tc", expected); + assertSuccess(parser, "a:\n\t b\n\t c", expected); + + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionDifferentIndent() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + assertFailure(parser, "a:\n b\n\tc", 6); + assertFailure(parser, "a:\n\tb\n c", 6); + + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionMissingIndent() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + assertSuccess(parser, "a:\nb", Arrays.asList("a:", "b")); + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionUnexpectedIndent() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + assertFailure(parser, "a\n b", 2); + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionSameLevel() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + assertSuccess(parser, "a\nb\nc", Arrays.asList("a", "b", "c")); + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionInlinedValues() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + List expected = Arrays.asList(mapOf("a", "1"), mapOf("b", "2"), mapOf("c", "3")); + assertSuccess(parser, "a:1\nb: 2\nc :3", expected); + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionIncreasing() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + List expected = Collections.singletonList( + mapOf("a", Collections.singletonList( + mapOf("b", Collections.singletonList("c"))))); + assertSuccess(parser, "a:\n b:\n c", expected); + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testDefinitionDecreasing() { + IndentList definition = new IndentList(); + Parser parser = definition.build(); + + List expected = Arrays.asList( + mapOf("a", Collections.singletonList("b")), + "c"); + assertSuccess(parser, "a:\n\tb\nc", expected); + assertTrue(definition.indent.getStack().isEmpty()); + assertEquals("", definition.indent.getCurrent()); + } + + @Test + public void testSuccessRestoresIndentation() { + Indent indent = new Indent(); + Parser inner = indent.same() + .then(CharacterParser.of('a')) + .map((indentStr, ch) -> "" + indentStr + ch); + Parser parser = indent.during(inner); + + assertSuccess(parser, " a", " a"); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testFailureRollsBackIndentation() { + Indent indent = new Indent(); + Parser inner = indent.same().seq(CharacterParser.of('a')); + Parser parser = indent.during(inner); + + assertFailure(parser, " b", 1, "'a' expected"); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testNestedFailureRollsBackEachLevel() { + Indent indent = new Indent(); + Parser inner = indent.during(indent.same().seq(CharacterParser.of('b'))); + Parser outer = indent.during(indent.same().seq(CharacterParser.of('\n')).seq(inner)); + + assertFailure(outer, " \n c", 4, "'b' expected"); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testChoiceRollbackAllowsAlternatePath() { + Indent indent = new Indent(); + Parser inner1 = indent.same().seq(CharacterParser.of('a')); + Parser inner2 = indent.same().seq(CharacterParser.of('b')); + Parser parser = indent.during(inner1).or(indent.during(inner2)); + + assertSuccess(parser, " b", Arrays.asList(" ", 'b')); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testIncreaseFailureLeavesStateUnchanged() { + Indent indent = new Indent(); + Parser inner = indent.same().seq(CharacterParser.of('a')); + Parser parser = indent.during(inner); + + assertFailure(parser, "a", 0, "indented expected"); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testRestoresNonEmptyParentIndentationOnSuccess() { + Indent indent = new Indent(); + Parser block = indent.during(indent.same().seq(CharacterParser.of('b'))); + Parser parser = indent.during(indent.same().seq(CharacterParser.of('a')).seq(block)); + + assertSuccess(parser, " a b", Arrays.asList(" ", 'a', Arrays.asList(" ", 'b'))); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testConsecutiveDuringBlocksAtSameLevel() { + Indent indent = new Indent(); + Parser block1 = indent.during(indent.same().seq(CharacterParser.of('a'))); + Parser block2 = indent.during(indent.same().seq(CharacterParser.of('b'))); + Parser parser = block1.seq(block2); + + assertSuccess(parser, " a b", Arrays.asList(Arrays.asList(" ", 'a'), Arrays.asList(" ", 'b'))); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testAcceptFastParseOnSuccessAndFailure() { + Indent indent = new Indent(); + Parser inner = indent.same().seq(CharacterParser.of('a')); + Parser parser = indent.during(inner); + + assertTrue(parser.accept(" a")); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + + assertFalse(parser.accept(" b")); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testCustomIndentationTokenParsersTabs() { + Indent tabIndent = new Indent(CharacterParser.of('\t'), "tab expected"); + Parser inner = tabIndent.same().seq(CharacterParser.of('x')); + Parser parser = tabIndent.during(inner); + + assertSuccess(parser, "\tx", Arrays.asList("\t", 'x')); + assertFailure(parser, " x", 0, "tab expected"); + assertEquals("", tabIndent.getCurrent()); + assertTrue(tabIndent.getStack().isEmpty()); + } + + @Test + public void testCustomIndentationTokenParsersSpaces() { + Indent spaceIndent = new Indent(CharacterParser.of(' '), "space expected"); + Parser inner = spaceIndent.same().seq(CharacterParser.of('x')); + Parser parser = spaceIndent.during(inner); + + assertSuccess(parser, " x", Arrays.asList(" ", 'x')); + assertFailure(parser, "\tx", 0, "space expected"); + assertEquals("", spaceIndent.getCurrent()); + assertTrue(spaceIndent.getStack().isEmpty()); + } + + @Test + public void testCustomIndentationTokenParsersString() { + Indent strict2Indent = new Indent(StringParser.of(" "), "two spaces expected"); + Parser inner = strict2Indent.same().seq(CharacterParser.of('x')); + Parser parser = strict2Indent.during(inner); + + assertSuccess(parser, " x", Arrays.asList(" ", 'x')); + assertFailure(parser, " x", 0, "two spaces expected"); + assertEquals("", strict2Indent.getCurrent()); + assertTrue(strict2Indent.getStack().isEmpty()); + } + + @Test + public void testDirectSameIncreaseDecrease() { + Indent indent = new Indent(); + + // same() at root matches empty indent + assertSuccess(indent.same(), "", ""); + assertFailure(indent.same(), " "); + + // increase() requires deeper indentation and pushes to stack + org.petitparser.context.Result increaseResult = indent.increase().parse(" "); + assertTrue(increaseResult.isSuccess()); + assertEquals(" ", increaseResult.get()); + assertEquals(0, increaseResult.getPosition()); + assertEquals(" ", indent.getCurrent()); + assertEquals(Collections.singletonList(""), indent.getStack()); + + // increase() with smaller/same length fails + assertFailure(indent.increase(), " "); + assertFailure(indent.increase(), " "); + + // test fastParseOn for increase after resetting + indent.reset(); + assertEquals(0, indent.increase().fastParseOn(" ", 0)); + assertEquals(" ", indent.getCurrent()); + assertEquals(Collections.singletonList(""), indent.getStack()); + + // decrease() pops from stack + org.petitparser.context.Result decreaseResult = indent.decrease().parse(""); + assertTrue(decreaseResult.isSuccess()); + assertNull(decreaseResult.get()); + assertEquals(0, decreaseResult.getPosition()); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + + // test fastParseOn for decrease with a pushed stack + indent.stack.add("parent"); + indent.current = "child"; + assertEquals(0, indent.decrease().fastParseOn("", 0)); + assertEquals("parent", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + + // decrease() on empty stack fails + assertFailure(indent.decrease(), ""); + } + + @Test + public void testReset() { + Indent indent = new Indent(); + indent.stack.add("parent"); + indent.current = "child"; + + assertEquals("child", indent.getCurrent()); + assertEquals(Collections.singletonList("parent"), indent.getStack()); + + indent.reset(); + + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testConstructorsAndGetters() { + Indent defaultIndent = new Indent(); + assertNotNull(defaultIndent.getParser()); + assertEquals("indented expected", defaultIndent.getMessage()); + assertTrue(defaultIndent.toString().contains("Indent")); + + Indent customParser = new Indent(CharacterParser.of(' ')); + assertNotNull(customParser.getParser()); + assertEquals("indented expected", customParser.getMessage()); + + Indent customMessage = new Indent("custom message"); + assertEquals("custom message", customMessage.getMessage()); + + Indent customBoth = new Indent(CharacterParser.of('\t'), "tab message"); + assertNotNull(customBoth.getParser()); + assertEquals("tab message", customBoth.getMessage()); + + Indent nullBoth = new Indent(null, null); + assertNotNull(nullBoth.getParser()); + assertEquals("indented expected", nullBoth.getMessage()); + } + + @Test + public void testDuringNullThrows() { + Indent indent = new Indent(); + try { + indent.during(null); + fail("Expected NullPointerException"); + } catch (NullPointerException e) { + assertEquals("Undefined inner parser", e.getMessage()); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void testStackUnmodifiable() { + Indent indent = new Indent(); + indent.getStack().add("corrupt"); + } + + @Test + public void testNestedCustomIndentationString() { + Indent indent2 = new Indent(StringParser.of(" "), "two spaces expected"); + Parser level3 = indent2.during(indent2.same().seq(CharacterParser.of('c'))); + Parser level2 = indent2.during(indent2.same().seq(CharacterParser.of('b')).seq(level3)); + Parser level1 = indent2.during(indent2.same().seq(CharacterParser.of('a')).seq(level2)); + + assertSuccess(level1, " a b c", Arrays.asList(" ", 'a', Arrays.asList(" ", 'b', Arrays.asList(" ", 'c')))); + assertEquals("", indent2.getCurrent()); + assertTrue(indent2.getStack().isEmpty()); + + // Fails when level 3 has only 5 spaces instead of 6 (cannot increase from 4 to 6) + assertFailure(level1, " a b c", 8); + assertEquals("", indent2.getCurrent()); + assertTrue(indent2.getStack().isEmpty()); + } + + @Test + public void testNestedChoiceRollbackDeep() { + Indent indent = new Indent(); + Parser deepInner = indent.during(indent.same().seq(CharacterParser.of('1'))); + Parser branch1 = indent.during(indent.same().seq(CharacterParser.of('a')).seq(deepInner)); + Parser branch2 = indent.during(indent.same().seq(CharacterParser.of('a')).seq(CharacterParser.of('2'))); + Parser parser = branch1.or(branch2); + + // branch1 deepInner fails on '2' where '1' is expected; choice backtracks and branch2 succeeds + assertSuccess(parser, " a2", Arrays.asList(" ", 'a', '2')); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testFastParseOnNestedScopes() { + Indent indent = new Indent(); + Parser inner = indent.during(indent.same().seq(CharacterParser.of('b'))); + Parser outer = indent.during(indent.same().seq(CharacterParser.of('a')).seq(inner)); + + assertTrue(outer.accept(" a b")); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + + assertFalse(outer.accept(" a c")); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } + + @Test + public void testMixedIndentationMismatch() { + Indent indent = new Indent(); + Parser inner = indent.same().seq(CharacterParser.of('x')); + Parser block = indent.during(inner); + + // Parent level is 2 spaces + indent.stack.add(""); + indent.current = " "; + + // Attempting deeper level with tab prefix fails because "\t" does not start with " " + assertFailure(block, "\tx", 0); + assertEquals(" ", indent.getCurrent()); + assertEquals(Collections.singletonList(""), indent.getStack()); + + indent.reset(); + } + + @Test + public void testMultipleParsesWithSameIndentInstance() { + Indent indent = new Indent(); + Parser block = indent.during(indent.same().seq(CharacterParser.of('x'))); + + // Run 1: success + assertSuccess(block, " x", Arrays.asList(" ", 'x')); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + + // Run 2: failure + assertFailure(block, " y", 2, "'x' expected"); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + + // Run 3: success again + assertSuccess(block, " x", Arrays.asList(" ", 'x')); + assertEquals("", indent.getCurrent()); + assertTrue(indent.getStack().isEmpty()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/tools/TypedGrammarDefinitionTest.java b/petitparser-core/src/test/java/org/petitparser/tools/TypedGrammarDefinitionTest.java new file mode 100644 index 0000000..d460a06 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/tools/TypedGrammarDefinitionTest.java @@ -0,0 +1,111 @@ +package org.petitparser.tools; + +import org.junit.Test; +import org.petitparser.parser.Parser; + +import java.util.function.Function; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.petitparser.Assertions.assertSuccess; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.of; + +/** + * Tests for {@link GrammarDefinition} using {@link Production}. + */ +public class TypedGrammarDefinitionTest { + + static final Production START = GrammarDefinition.production("start"); + static final Production ADD = GrammarDefinition.production("add"); + static final Production NUMBER = GrammarDefinition.production("number"); + + static class TypedArithmeticGrammarDefinition extends GrammarDefinition { + TypedArithmeticGrammarDefinition() { + def(START, ref(ADD).end()); + def(ADD, Parser.seq(ref(NUMBER), of('+').trim(), ref(NUMBER)) + .map((a, op, b) -> (Integer) a + (Integer) b)); + def(NUMBER, digit().plus().flatten().trim().map((Function) Integer::parseInt)); + } + } + + static class TypedArithmeticParserDefinition extends TypedArithmeticGrammarDefinition { + TypedArithmeticParserDefinition() { + action(NUMBER, (Function) n -> n * 10); + } + } + + @Test + public void testProductionProperties() { + Production prod1 = Production.of("foo"); + Production prod2 = GrammarDefinition.production("foo"); + Production prod3 = Production.of("bar"); + + assertEquals("foo", prod1.getName()); + assertEquals(prod1, prod1); + assertEquals(prod1, prod2); + assertEquals(prod1.hashCode(), prod2.hashCode()); + assertNotEquals(prod1, prod3); + assertNotEquals(prod1, null); + assertNotEquals(prod1, "foo"); + assertEquals("Production[foo]", prod1.toString()); + + try { + Production.of(null); + org.junit.Assert.fail("Expected NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testTypedGrammarBuild() { + TypedArithmeticGrammarDefinition definition = new TypedArithmeticGrammarDefinition(); + Parser parser = definition.build(START); + + assertSuccess(parser, "12 + 34", 46); + } + + @Test + public void testTypedGrammarParser() { + TypedArithmeticGrammarDefinition definition = new TypedArithmeticGrammarDefinition(); + GrammarParser parser = new GrammarParser(definition, START); + + assertSuccess(parser, "5 + 7", 12); + } + + @Test + public void testTypedAction() { + TypedArithmeticParserDefinition definition = new TypedArithmeticParserDefinition(); + Parser parser = definition.build(START); + + // Number multiplied by 10 in action: 10 + 20 = 30 + assertSuccess(parser, "1 + 2", 30); + } + + @Test + public void testRedef() { + Production ITEM = Production.of("item"); + GrammarDefinition def = new GrammarDefinition() { + { + def(ITEM, of('a')); + redef(ITEM, of('b')); + } + }; + Parser parser = def.build(ITEM); + assertSuccess(parser, "b", 'b'); + } + + @Test + public void testRedefWithFunction() { + Production ITEM = Production.of("item"); + GrammarDefinition def = new GrammarDefinition() { + { + def(ITEM, of('a')); + redef(ITEM, (Parser p) -> p.seq(of('b'))); + } + }; + Parser parser = def.build(ITEM); + assertSuccess(parser, "ab", java.util.Arrays.asList('a', 'b')); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/utils/AnalyzerTest.java b/petitparser-core/src/test/java/org/petitparser/utils/AnalyzerTest.java new file mode 100644 index 0000000..8a32188 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/utils/AnalyzerTest.java @@ -0,0 +1,778 @@ +package org.petitparser.utils; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ResolvableParser; +import org.petitparser.parser.combinators.SettableParser; +import org.petitparser.parser.primitive.CharacterParser; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.combinators.SettableParser.undefined; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.letter; +import static org.petitparser.parser.primitive.CharacterParser.lowerCase; +import static org.petitparser.parser.primitive.CharacterParser.upperCase; + +/** + * Tests for {@link Analyzer}. + */ +public class AnalyzerTest { + + @Test(expected = NullPointerException.class) + public void testNullRoot() { + Analyzer.of(null); + } + + @Test + public void testGetRootAndToString() { + Parser parser = lowerCase(); + Analyzer analyzer = Analyzer.of(parser); + assertSame(parser, analyzer.getRoot()); + assertEquals("Analyzer of " + parser, analyzer.toString()); + } + + @Test + public void testSingleParserDiscovery() { + Parser parser = lowerCase(); + Analyzer analyzer = Analyzer.of(parser); + assertEquals(Collections.singletonList(parser), analyzer.parsers()); + assertEquals(Collections.singletonList(parser), analyzer.getParsers()); + assertEquals(Collections.singletonList(parser), + analyzer.stream().collect(Collectors.toList())); + } + + @Test + public void testNestedParsersDiscovery() { + Parser p3 = lowerCase(); + Parser p2 = p3.star(); + Parser p1 = p2.flatten(); + Analyzer analyzer = Analyzer.of(p1); + assertEquals(Arrays.asList(p1, p2, p3), analyzer.parsers()); + } + + @Test + public void testBranchedParsersDiscovery() { + Parser p3 = lowerCase(); + Parser p2 = upperCase(); + Parser p1 = p2.seq(p3); + Analyzer analyzer = Analyzer.of(p1); + assertEquals(Arrays.asList(p1, p2, p3), analyzer.parsers()); + } + + @Test + public void testDuplicateParsersDiscovery() { + Parser p2 = upperCase(); + Parser p1 = p2.seq(p2); + Analyzer analyzer = Analyzer.of(p1); + assertEquals(Arrays.asList(p1, p2), analyzer.parsers()); + } + + @Test + public void testKnotParserDiscovery() { + SettableParser p1 = undefined(); + p1.set(p1); + Analyzer analyzer = Analyzer.of(p1); + assertEquals(Collections.singletonList(p1), analyzer.parsers()); + } + + @Test + public void testLoopingParserDiscovery() { + SettableParser p1 = undefined(); + SettableParser p2 = undefined(); + SettableParser p3 = undefined(); + p1.set(p2); + p2.set(p3); + p3.set(p1); + Analyzer analyzer = Analyzer.of(p1); + assertEquals(Arrays.asList(p1, p2, p3), analyzer.parsers()); + } + + @Test + public void testAllChildren() { + Parser p3 = lowerCase(); + Parser p2 = p3.star(); + Parser p1 = p2.flatten(); + Analyzer analyzer = Analyzer.of(p1); + + assertTrue(analyzer.allChildren(p3).isEmpty()); + assertEquals(Collections.singleton(p3), analyzer.allChildren(p2)); + assertEquals(Set.of(p2, p3), analyzer.allChildren(p1)); + assertSame(analyzer.allChildren(p1), analyzer.allChildren(p1)); + } + + @Test + public void testAllChildrenCycles() { + SettableParser p1 = undefined(); + SettableParser p2 = undefined(); + p1.set(p2); + p2.set(p1); + Analyzer analyzer = Analyzer.of(p1); + + assertEquals(Set.of(p1, p2), analyzer.allChildren(p1)); + assertEquals(Set.of(p1, p2), analyzer.allChildren(p2)); + } + + @Test + public void testFindPathDirectAndTransitive() { + Parser p3 = lowerCase(); + Parser p2 = p3.star(); + Parser p1 = p2.flatten(); + Analyzer analyzer = Analyzer.of(p1); + + assertEquals(Collections.singletonList(p1), analyzer.findPath(p1, p1)); + assertEquals(Arrays.asList(p1, p2), analyzer.findPath(p1, p2)); + assertEquals(Arrays.asList(p1, p2, p3), analyzer.findPath(p1, p3)); + assertEquals(Arrays.asList(p1, p2, p3), analyzer.findPathTo(p3)); + + Parser unrelated = digit(); + assertTrue(analyzer.findPath(p1, unrelated).isEmpty()); + assertTrue(analyzer.findPathTo(unrelated).isEmpty()); + } + + @Test + public void testFindPathShortest() { + Parser target = digit(); + Parser longBranch = lowerCase().seq(target); + Parser shortBranch = target; + Parser root = longBranch.or(shortBranch); + Analyzer analyzer = Analyzer.of(root); + + List path = analyzer.findPath(root, target); + assertEquals(Arrays.asList(root, target), path); + } + + @Test + public void testFindPathWithPredicate() { + Parser p3 = lowerCase(); + Parser p2 = p3.star(); + Parser p1 = p2.flatten(); + Analyzer analyzer = Analyzer.of(p1); + + List path = analyzer.findPathTo(p -> p instanceof CharacterParser); + assertEquals(Arrays.asList(p1, p2, p3), path); + + List notFound = analyzer.findPath(p1, p -> false); + assertTrue(notFound.isEmpty()); + } + + @Test + public void testFindAllPathsDiamond() { + Parser target = digit(); + Parser b = target.star(); + Parser c = target.plus(); + Parser a = b.seq(c); + Analyzer analyzer = Analyzer.of(a); + + List> paths = analyzer.findAllPathsTo(target); + assertEquals(2, paths.size()); + assertTrue(paths.contains(Arrays.asList(a, b, target))); + assertTrue(paths.contains(Arrays.asList(a, c, target))); + + assertEquals(paths, analyzer.findAllPaths(a, target)); + assertEquals(paths, analyzer.findAllPathsTo(p -> p == target)); + } + + @Test + public void testFindAllPathsCycle() { + SettableParser p1 = undefined(); + Parser target = digit(); + p1.set(target.or(p1)); + Analyzer analyzer = Analyzer.of(p1); + + List> paths = analyzer.findAllPathsTo(target); + assertEquals(1, paths.size()); + assertEquals(Arrays.asList(p1, p1.getChildren().get(0), target), paths.get(0)); + } + + @Test + public void testFindCycle() { + Parser p1 = letter().star(); + Analyzer analyzer = Analyzer.of(p1); + assertTrue(analyzer.findCycle(p1).isEmpty()); + + SettableParser knot = undefined(); + knot.set(knot); + Analyzer knotAnalyzer = Analyzer.of(knot); + assertEquals(Arrays.asList(knot, knot), knotAnalyzer.findCycle(knot)); + + SettableParser c1 = undefined(); + SettableParser c2 = undefined(); + c1.set(c2); + c2.set(c1); + Analyzer cycleAnalyzer = Analyzer.of(c1); + assertEquals(Arrays.asList(c1, c2, c1), cycleAnalyzer.findCycle(c1)); + assertEquals(Arrays.asList(c2, c1, c2), cycleAnalyzer.findCycle(c2)); + } + + @Test + public void testCycleSetAndIsCyclic() { + Parser acyclic = letter().seq(digit()); + Analyzer analyzer = Analyzer.of(acyclic); + assertFalse(analyzer.isCyclic()); + assertFalse(analyzer.isCyclic(acyclic)); + assertTrue(analyzer.cycleSet(acyclic).isEmpty()); + assertTrue(analyzer.cycles().isEmpty()); + + SettableParser knot = undefined(); + knot.set(knot); + Analyzer knotAnalyzer = Analyzer.of(knot); + assertTrue(knotAnalyzer.isCyclic()); + assertTrue(knotAnalyzer.isCyclic(knot)); + assertEquals(Set.of(knot), knotAnalyzer.cycleSet(knot)); + assertEquals(Set.of(knot), knotAnalyzer.cycles()); + + SettableParser c1 = undefined(); + SettableParser c2 = undefined(); + c1.set(c2); + c2.set(c1); + Analyzer cycleAnalyzer = Analyzer.of(c1); + assertTrue(cycleAnalyzer.isCyclic(c1)); + assertTrue(cycleAnalyzer.isCyclic(c2)); + assertEquals(Set.of(c1, c2), cycleAnalyzer.cycleSet(c1)); + assertEquals(Set.of(c1, c2), cycleAnalyzer.cycleSet(c2)); + + Parser root = letter().seq(c1); + Analyzer rootAnalyzer = Analyzer.of(root); + assertTrue(rootAnalyzer.isCyclic()); + assertFalse(rootAnalyzer.isCyclic(root)); + assertTrue(rootAnalyzer.cycleSet(root).isEmpty()); + assertTrue(rootAnalyzer.isCyclic(c1)); + assertEquals(Set.of(c1, c2), rootAnalyzer.cycles()); + } + + @Test + public void testIsNullableTerminalsAndCombinators() { + Parser letter = letter(); + Parser eps = org.petitparser.parser.primitive.EpsilonParser.INSTANCE; + Parser pos = org.petitparser.parser.primitive.PositionParser.INSTANCE; + Parser fail = org.petitparser.parser.primitive.FailureParser.withMessage("err"); + + Parser root = letter.seq(eps).seq(pos).seq(fail); + Analyzer analyzer = Analyzer.of(root); + + assertFalse(analyzer.isNullable(letter)); + assertTrue(analyzer.isNullable(eps)); + assertTrue(analyzer.isNullable(pos)); + assertFalse(analyzer.isNullable(fail)); + + assertTrue(analyzer.isNullable(letter.optional())); + assertTrue(analyzer.isNullable(letter.star())); + assertFalse(analyzer.isNullable(letter.plus())); + assertTrue(analyzer.isNullable(letter.repeat(0, 3))); + assertFalse(analyzer.isNullable(letter.repeat(1, 3))); + + assertTrue(analyzer.isNullable(letter.repeatSeparated(digit(), 0, 3))); + assertFalse(analyzer.isNullable(letter.repeatSeparated(digit(), 1, 3))); + assertTrue(analyzer.isNullable(letter.starSeparated(digit()))); + assertFalse(analyzer.isNullable(letter.plusSeparated(digit()))); + + // Sequences + assertTrue(analyzer.isNullable(eps.seq(letter.star()))); + assertFalse(analyzer.isNullable(eps.seq(letter))); + + // Choices + assertTrue(analyzer.isNullable(letter.or(eps))); + assertFalse(analyzer.isNullable(letter.or(digit()))); + + // Trimming + assertFalse(analyzer.isNullable(letter.trim())); + assertTrue(analyzer.isNullable(letter.optional().trim())); + + // Not & And + assertTrue(analyzer.isNullable(letter.not())); + assertFalse(analyzer.isNullable(eps.not())); + assertFalse(analyzer.isNullable(letter.and())); + assertTrue(analyzer.isNullable(eps.and())); + } + + @Test + public void testIsNullableRecursive() { + SettableParser p = undefined(); + p.set(p.or(org.petitparser.parser.primitive.EpsilonParser.INSTANCE)); + Analyzer analyzer = Analyzer.of(p); + assertTrue(analyzer.isNullable(p)); + + SettableParser nonNull = undefined(); + nonNull.set(nonNull.seq(digit()).or(letter())); + Analyzer nonNullAnalyzer = Analyzer.of(nonNull); + assertFalse(nonNullAnalyzer.isNullable(nonNull)); + } + + @Test + public void testFirstSet() { + Parser a = CharacterParser.of('a'); + Parser b = CharacterParser.of('b'); + Parser c = CharacterParser.of('c'); + + Analyzer aAnalyzer = Analyzer.of(a); + assertEquals(Set.of(a), aAnalyzer.firstSet(a)); + + Parser seq = a.seq(b); + Analyzer seqAnalyzer = Analyzer.of(seq); + assertEquals(Set.of(a), seqAnalyzer.firstSet(seq)); + + Parser nullableSeq = a.optional().seq(b); + Analyzer nullableSeqAnalyzer = Analyzer.of(nullableSeq); + assertEquals(Set.of(a, b), nullableSeqAnalyzer.firstSet(nullableSeq)); + + Parser allNullableSeq = a.optional().seq(b.optional()).seq(c); + Analyzer allNullableSeqAnalyzer = Analyzer.of(allNullableSeq); + assertEquals(Set.of(a, b, c), allNullableSeqAnalyzer.firstSet(allNullableSeq)); + + Parser choice = a.or(b); + Analyzer choiceAnalyzer = Analyzer.of(choice); + assertEquals(Set.of(a, b), choiceAnalyzer.firstSet(choice)); + + Parser sep = a.separatedBy(b); + Analyzer sepAnalyzer = Analyzer.of(sep); + assertEquals(Set.of(a), sepAnalyzer.firstSet(sep)); + } + + @Test + public void testFirstSetLeftRecursive() { + SettableParser expr = undefined(); + Parser plus = CharacterParser.of('+'); + Parser num = digit(); + expr.set(expr.seq(plus).seq(num).or(num)); + Analyzer analyzer = Analyzer.of(expr); + + assertEquals(Set.of(num), analyzer.firstSet(expr)); + } + + @Test + public void testFollowSet() { + Parser a = CharacterParser.of('a'); + Parser b = CharacterParser.of('b'); + Parser c = CharacterParser.of('c'); + + Parser seq = a.seq(b); + Analyzer seqAnalyzer = Analyzer.of(seq); + assertEquals(Set.of(b), seqAnalyzer.followSet(a)); + assertTrue(seqAnalyzer.followSet(b).isEmpty()); + + Parser nullableIntermediate = a.seq(b.optional()).seq(c); + Analyzer nullableAnalyzer = Analyzer.of(nullableIntermediate); + assertEquals(Set.of(b, c), nullableAnalyzer.followSet(a)); + assertEquals(Set.of(c), nullableAnalyzer.followSet(b)); + + Parser loop = a.seq(b).star(); + Analyzer loopAnalyzer = Analyzer.of(loop); + assertEquals(Set.of(a), loopAnalyzer.followSet(b)); + + Parser sepList = a.separatedBy(b); + Analyzer sepAnalyzer = Analyzer.of(sepList); + assertEquals(Set.of(b), sepAnalyzer.followSet(a)); + assertEquals(Set.of(a), sepAnalyzer.followSet(b)); + } + + @Test + public void testResolveNonResolvable() { + Parser parser = letter().star(); + assertSame(parser, Analyzer.resolve(parser)); + assertSame(parser, Analyzer.of(parser).resolve()); + } + + @Test + public void testResolveSingleSettable() { + CharacterParser inner = CharacterParser.of('a'); + SettableParser settable = SettableParser.with(inner); + Parser resolved = Analyzer.resolve(settable); + assertSame(inner, resolved); + } + + @Test + public void testResolveChainedSettable() { + CharacterParser inner = CharacterParser.of('x'); + SettableParser s1 = undefined(); + SettableParser s2 = undefined(); + s1.set(s2); + s2.set(inner); + Parser resolved = Analyzer.resolve(s1); + assertSame(inner, resolved); + } + + @Test(expected = IllegalStateException.class) + public void testResolveCyclicSettableError() { + SettableParser s1 = undefined(); + SettableParser s2 = undefined(); + s1.set(s2); + s2.set(s1); + Analyzer.resolve(s1); + } + + @Test + public void testResolveInTree() { + CharacterParser a = CharacterParser.of('a'); + CharacterParser b = CharacterParser.of('b'); + SettableParser s = undefined(); + Parser seq = a.seq(s); + s.set(b); + + Parser resolved = Analyzer.resolve(seq); + assertSame(seq, resolved); + assertEquals(Arrays.asList(a, b), resolved.getChildren()); + org.petitparser.Assertions.assertSuccess(resolved, "ab", Arrays.asList('a', 'b')); + } + + @Test + public void testResolveRecursiveGrammar() { + SettableParser expr = undefined(); + Parser prim = digit().plus().flatten(); + Parser group = CharacterParser.of('(').seq(expr).seq(CharacterParser.of(')')).pick(1); + expr.set(prim.or(group)); + + Parser resolved = Analyzer.of(expr).resolve(); + assertFalse(resolved instanceof org.petitparser.parser.combinators.ResolvableParser); + + List all = Analyzer.of(resolved).parsers(); + for (Parser p : all) { + assertFalse(p instanceof org.petitparser.parser.combinators.ResolvableParser); + } + + org.petitparser.Assertions.assertSuccess(resolved, "42", "42"); + org.petitparser.Assertions.assertSuccess(resolved, "(42)", "42"); + org.petitparser.Assertions.assertSuccess(resolved, "((123))", "123"); + org.petitparser.Assertions.assertFailure(resolved, "(42", 3, "')' expected"); + } + + @Test + public void testDragonBookLL1Grammar() { + // E -> T E' + // E' -> + T E' | epsilon + // T -> F T' + // T' -> * F T' | epsilon + // F -> ( E ) | id + SettableParser e = undefined(); + SettableParser ePrime = undefined(); + SettableParser t = undefined(); + SettableParser tPrime = undefined(); + SettableParser f = undefined(); + + Parser plus = CharacterParser.of('+'); + Parser star = CharacterParser.of('*'); + Parser lparen = CharacterParser.of('('); + Parser rparen = CharacterParser.of(')'); + Parser id = letter(); + + f.set(lparen.seq(e).seq(rparen).or(id)); + tPrime.set(star.seq(f).seq(tPrime).or(org.petitparser.parser.primitive.EpsilonParser.INSTANCE)); + t.set(f.seq(tPrime)); + ePrime.set(plus.seq(t).seq(ePrime).or(org.petitparser.parser.primitive.EpsilonParser.INSTANCE)); + e.set(t.seq(ePrime)); + + Analyzer analyzer = Analyzer.of(e); + + // Nullability + assertFalse(analyzer.isNullable(e)); + assertTrue(analyzer.isNullable(ePrime)); + assertFalse(analyzer.isNullable(t)); + assertTrue(analyzer.isNullable(tPrime)); + assertFalse(analyzer.isNullable(f)); + + // FIRST sets + assertEquals(Set.of(lparen, id), analyzer.firstSet(f)); + assertEquals(Set.of(star), analyzer.firstSet(tPrime)); + assertEquals(Set.of(lparen, id), analyzer.firstSet(t)); + assertEquals(Set.of(plus), analyzer.firstSet(ePrime)); + assertEquals(Set.of(lparen, id), analyzer.firstSet(e)); + + // FOLLOW sets + assertEquals(Set.of(rparen), analyzer.followSet(e)); + assertEquals(Set.of(rparen), analyzer.followSet(ePrime)); + assertEquals(Set.of(plus, rparen), analyzer.followSet(t)); + assertEquals(Set.of(plus, rparen), analyzer.followSet(tPrime)); + assertEquals(Set.of(star, plus, rparen), analyzer.followSet(f)); + } + + @Test(expected = NullPointerException.class) + public void testAllChildrenNull() { + Analyzer.of(digit()).allChildren(null); + } + + @Test(expected = NullPointerException.class) + public void testIsNullableNull() { + Analyzer.of(digit()).isNullable(null); + } + + @Test(expected = NullPointerException.class) + public void testFirstSetNull() { + Analyzer.of(digit()).firstSet(null); + } + + @Test(expected = NullPointerException.class) + public void testFollowSetNull() { + Analyzer.of(digit()).followSet(null); + } + + @Test(expected = NullPointerException.class) + public void testCycleSetNull() { + Analyzer.of(digit()).cycleSet(null); + } + + @Test(expected = NullPointerException.class) + public void testFindPathNullSource() { + Analyzer.of(digit()).findPath(null, digit()); + } + + @Test(expected = NullPointerException.class) + public void testFindPathNullTarget() { + Analyzer.of(digit()).findPath(digit(), (Parser) null); + } + + @Test(expected = NullPointerException.class) + public void testFindPathNullPredicate() { + Analyzer.of(digit()).findPath(digit(), (java.util.function.Predicate) null); + } + + @Test(expected = NullPointerException.class) + public void testFindCycleNull() { + Analyzer.of(digit()).findCycle(null); + } + + @Test(expected = NullPointerException.class) + public void testResolveNull() { + Analyzer.resolve(null); + } + + @Test + public void testUnrelatedParserQueries() { + Parser a = CharacterParser.of('a'); + Analyzer analyzer = Analyzer.of(a); + + Parser unrelated = CharacterParser.of('z'); + assertFalse(analyzer.isNullable(unrelated)); + assertEquals(Set.of(unrelated), analyzer.firstSet(unrelated)); + assertTrue(analyzer.followSet(unrelated).isEmpty()); + } + + @Test + public void testIsNullableSettableNot() { + SettableParser s = undefined(); + s.set(org.petitparser.parser.primitive.EpsilonParser.INSTANCE); + Parser notS = s.not(); + assertFalse(Analyzer.of(notS).isNullable(notS)); + } + + @Test + public void testIsNullableSeparatedRepeating() { + Parser nullableDelegate = CharacterParser.of('a').optional(); + Parser nonNullableSep = CharacterParser.of(','); + Parser srp = nullableDelegate.plusSeparated(nonNullableSep); + assertTrue(Analyzer.of(srp).isNullable(srp)); + + Parser srp2 = nullableDelegate.repeatSeparated(nonNullableSep, 2, 5); + assertFalse(Analyzer.of(srp2).isNullable(srp2)); + + Parser srp2Nullable = nullableDelegate.repeatSeparated(nonNullableSep.optional(), 2, 5); + assertTrue(Analyzer.of(srp2Nullable).isNullable(srp2Nullable)); + } + + @Test + public void testIsNullableSpecialParsers() { + Parser starString = digit().starString(); + assertTrue(Analyzer.of(starString).isNullable(starString)); + + Parser plusString = digit().plusString(); + assertFalse(Analyzer.of(plusString).isNullable(plusString)); + + Parser eoi = new org.petitparser.parser.combinators.EndOfInputParser("end"); + assertTrue(Analyzer.of(eoi).isNullable(eoi)); + + Parser emptyStr = org.petitparser.parser.primitive.StringParser.of(""); + assertTrue(Analyzer.of(emptyStr).isNullable(emptyStr)); + + Parser nonEmptyStr = org.petitparser.parser.primitive.StringParser.of("foo"); + assertFalse(Analyzer.of(nonEmptyStr).isNullable(nonEmptyStr)); + } + + @Test + public void testFirstSetNotParser() { + Parser a = CharacterParser.of('a'); + Parser b = CharacterParser.of('b'); + + assertEquals(Collections.emptySet(), Analyzer.of(a.not()).firstSet(a.not())); + + Parser seq = a.not().seq(b); + assertEquals(Set.of(b), Analyzer.of(seq).firstSet(seq)); + } + + @Test + public void testFirstSetSeparatedRepeating() { + Parser a = CharacterParser.of('a'); + Parser b = CharacterParser.of('b'); + + Parser srp = a.plusSeparated(b); + assertEquals(Set.of(a), Analyzer.of(srp).firstSet(srp)); + + Parser nullableSrp = a.optional().plusSeparated(b); + assertEquals(Set.of(a, b), Analyzer.of(nullableSrp).firstSet(nullableSrp)); + } + + @Test + public void testFollowSetSeparatedRepeating() { + Parser a = CharacterParser.of('a'); + Parser b = CharacterParser.of('b'); + Parser c = CharacterParser.of('c'); + + Parser srp = a.plusSeparated(b); + Analyzer srpAnalyzer = Analyzer.of(srp); + assertEquals(Set.of(a), srpAnalyzer.followSet(b)); + assertEquals(Set.of(b), srpAnalyzer.followSet(a)); + + Parser seq = srp.seq(c); + Analyzer seqAnalyzer = Analyzer.of(seq); + assertEquals(Set.of(b, c), seqAnalyzer.followSet(a)); + assertEquals(Set.of(a), seqAnalyzer.followSet(b)); + + Parser nullableSep = a.plusSeparated(b.optional()); + Analyzer nullableSepAnalyzer = Analyzer.of(nullableSep); + assertEquals(Set.of(a, b), nullableSepAnalyzer.followSet(a)); + } + + @Test + public void testFindAllPathsNestedMatchingPredicate() { + Parser a = digit(); + Parser b = letter(); + Parser c = lowerCase(); + Parser innerSeq = b.seq(c); + Parser outerSeq = a.seq(innerSeq); + Analyzer analyzer = Analyzer.of(outerSeq); + + List> paths = analyzer.findAllPathsTo(p -> p instanceof org.petitparser.parser.combinators.SequenceParser); + assertEquals(2, paths.size()); + assertTrue(paths.contains(Collections.singletonList(outerSeq))); + assertTrue(paths.contains(Arrays.asList(outerSeq, innerSeq))); + } + + @Test + public void testFindCycleShortest() { + SettableParser root = undefined(); + SettableParser longCycle = undefined(); + SettableParser shortCycle = undefined(); + + root.set(longCycle.or(shortCycle)); + longCycle.set(digit().seq(root)); + shortCycle.set(root); + + Analyzer analyzer = Analyzer.of(root); + List cycle = analyzer.findCycle(root); + // Shortest cycle path is root -> choice -> shortCycle -> root + assertEquals(Arrays.asList(root, root.getChildren().get(0), shortCycle, root), cycle); + } + + @Test + public void testResolveInvalidatesAnalyzerCaches() { + SettableParser s = undefined(); + Parser inner = digit(); + Parser first = letter(); + Parser root = first.seq(s); + s.set(inner); + + Analyzer analyzer = Analyzer.of(root); + assertEquals(4, analyzer.parsers().size()); + + Parser resolved = analyzer.resolve(); + assertSame(root, resolved); + assertEquals(Arrays.asList(first, inner), resolved.getChildren()); + assertEquals(3, analyzer.parsers().size()); + } + + @Test + public void testIsNullableTrimmingParser() { + Parser a = CharacterParser.of('a'); + Parser ws = CharacterParser.whitespace(); + + Parser nonNullableTrim = a.trim(ws, ws); + assertFalse(Analyzer.of(nonNullableTrim).isNullable(nonNullableTrim)); + + Parser nullableTrim = a.optional().trim(ws, ws); + assertTrue(Analyzer.of(nullableTrim).isNullable(nullableTrim)); + } + + @Test + public void testFirstSetTrimmingParser() { + Parser del = CharacterParser.of('a'); + Parser left = CharacterParser.of('['); + Parser right = CharacterParser.of(']'); + + Parser trimNonNullable = del.trim(left, right); + Analyzer a1 = Analyzer.of(trimNonNullable); + // When del is not nullable, firstSet is {left, del} + assertEquals(Set.of(left, del), a1.firstSet(trimNonNullable)); + + Parser trimNullable = del.optional().trim(left, right); + Analyzer a2 = Analyzer.of(trimNullable); + // When del is nullable, firstSet includes right as well: {left, del, right} + assertEquals(Set.of(left, del, right), a2.firstSet(trimNullable)); + } + + @Test + public void testFollowSetTrimmingParser() { + Parser del = CharacterParser.of('a'); + Parser left = CharacterParser.of('['); + Parser right = CharacterParser.of(']'); + Parser end = CharacterParser.of('$'); + + // Case 1: del is not nullable + Parser trimNonNullable = del.trim(left, right).seq(end); + Analyzer a1 = Analyzer.of(trimNonNullable); + assertEquals(Set.of(left, del), a1.followSet(left)); + assertEquals(Set.of(right, end), a1.followSet(del)); + assertEquals(Set.of(right, end), a1.followSet(right)); + + // Case 2: del is nullable + Parser trimNullable = del.optional().trim(left, right).seq(end); + Analyzer a2 = Analyzer.of(trimNullable); + // When del is nullable, followSet(left) gets firstSet(right) and followSet(p) = {end} + assertTrue(a2.followSet(left).contains(right)); + assertTrue(a2.followSet(left).contains(end)); + assertTrue(a2.followSet(del).contains(right)); + assertTrue(a2.followSet(del).contains(end)); + assertTrue(a2.followSet(right).contains(right)); + assertTrue(a2.followSet(right).contains(end)); + } + + @Test + public void testFollowSetSeparatedRepeatingNullableDelegate() { + Parser del = CharacterParser.of('a').optional(); + Parser sep = CharacterParser.of(','); + Parser end = CharacterParser.of('$'); + Parser separated = del.starSeparated(sep).seq(end); + Analyzer a = Analyzer.of(separated); + + assertTrue(a.followSet(sep).contains(sep)); + assertTrue(a.followSet(sep).contains(end)); + } + + @Test + public void testResolveUnreachableTargetSubtree() { + CharacterParser leaf = CharacterParser.of('z'); + Parser outside = leaf.seq(CharacterParser.of('!')); + class TestResolvable extends org.petitparser.parser.combinators.DelegateParser implements ResolvableParser { + TestResolvable() { + super(CharacterParser.of('x')); + } + + @Override + public Parser resolve() { + return outside; + } + } + + SettableParser root = undefined(); + root.set(new TestResolvable()); + + Parser resolved = Analyzer.resolve(root); + assertSame(outside, resolved); + assertFalse(Analyzer.of(resolved).findPathTo(leaf).isEmpty()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/utils/LinterTest.java b/petitparser-core/src/test/java/org/petitparser/utils/LinterTest.java new file mode 100644 index 0000000..c56e172 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/utils/LinterTest.java @@ -0,0 +1,106 @@ +package org.petitparser.utils; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.SettableParser; +import org.petitparser.utils.linter.CharacterRepeaterRule; +import org.petitparser.utils.linter.DuplicateParserRule; +import org.petitparser.utils.linter.LinterIssue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.of; + +/** + * Parallel unit tests for facade {@link Linter}. + */ +public class LinterTest { + + @Test + public void testLintDefault() { + Parser parser = of('a'); + List issues = Linter.lint(parser); + assertTrue(issues.isEmpty()); + } + + @Test + public void testLintWithRulesList() { + Parser parser = digit().seq(digit()); + List issues = Linter.lint(parser, List.of(new DuplicateParserRule())); + assertEquals(1, issues.size()); + assertEquals("Duplicate parser", issues.get(0).getTitle()); + } + + @Test + public void testLintWithRulesVarargs() { + Parser parser = of('a').star().flatten(); + List issues = Linter.lint(parser, new CharacterRepeaterRule(), new DuplicateParserRule()); + assertEquals(1, issues.size()); + assertEquals("Character repeater", issues.get(0).getTitle()); + } + + @Test + public void testLintWithCallback() { + Parser parser = SettableParser.undefined(); + List callbackIssues = new ArrayList<>(); + List issues = Linter.lint(parser, callbackIssues::add); + assertFalse(issues.isEmpty()); + assertEquals(issues, callbackIssues); + assertTrue(issues.stream().anyMatch(i -> i.getTitle().equals("Unresolved settable"))); + assertTrue(issues.stream().anyMatch(i -> i.getTitle().equals("Unnecessary resolvable"))); + } + + @Test + public void testLintWithCallbackAndRulesList() { + Parser parser = of('a').star().flatten(); + AtomicInteger count = new AtomicInteger(); + List issues = Linter.lint( + parser, + issue -> count.incrementAndGet(), + List.of(new CharacterRepeaterRule()) + ); + assertEquals(1, issues.size()); + assertEquals(1, count.get()); + } + + @Test + public void testLintFullyConfigured() { + Parser parser = of('a').or(of('b').or(of('c'))); // Nested choice is INFO + List issues = Linter.lint( + parser, + null, + null, + Collections.emptySet(), + Collections.emptySet() // include INFO + ); + assertFalse(issues.isEmpty()); + assertEquals("Nested choice", issues.get(0).getTitle()); + } + + @Test + public void testQueryFacade() { + Parser parser = of('a').star().flatten(); + assertNotNull(Linter.query(parser)); + List issues = Linter.query(parser) + .rules(new CharacterRepeaterRule()) + .lint(); + assertEquals(1, issues.size()); + assertEquals("Character repeater", issues.get(0).getTitle()); + + List listRules = Linter.query(parser) + .rules(Collections.singletonList(new CharacterRepeaterRule())) + .lint(); + assertEquals(1, listRules.size()); + + List defaultIssues = Linter.query(of('a')).lint(); + assertTrue(defaultIssues.isEmpty()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/utils/OptimizerTest.java b/petitparser-core/src/test/java/org/petitparser/utils/OptimizerTest.java index 50b4959..908039b 100644 --- a/petitparser-core/src/test/java/org/petitparser/utils/OptimizerTest.java +++ b/petitparser-core/src/test/java/org/petitparser/utils/OptimizerTest.java @@ -1,12 +1,35 @@ package org.petitparser.utils; import org.junit.Test; +import org.petitparser.context.Result; import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ChoiceParser; +import org.petitparser.parser.combinators.SettableParser; +import org.petitparser.parser.combinators.DelegateParser; +import org.petitparser.parser.primitive.CharacterParser; +import org.petitparser.parser.primitive.UnicodeCharacterParser; +import org.petitparser.parser.repeating.RepeatingCharacterParser; +import org.petitparser.utils.optimizer.CharacterRepeaterRule; +import org.petitparser.utils.optimizer.FlattenChoiceRule; +import org.petitparser.utils.optimizer.OptimizeRule; +import org.petitparser.utils.optimizer.RemoveDelegateRule; +import org.petitparser.utils.optimizer.RemoveDuplicateRule; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.primitive.CharacterParser.digit; import static org.petitparser.parser.primitive.CharacterParser.lowerCase; +import static org.petitparser.parser.primitive.CharacterParser.of; /** * Tests {@link Optimizer}. @@ -49,4 +72,646 @@ public void testRemoveDuplicates() { assertNotEquals(input.getChildren().get(0), input.getChildren().get(1)); assertEquals(output.getChildren().get(0), output.getChildren().get(1)); } + + @Test + public void testRemoveDuplicatesWithCustomSet() { + Set customSet = new HashSet<>(); + Parser firstLower = lowerCase(); + customSet.add(firstLower); + + Parser input = lowerCase().seq(lowerCase()); + Parser output = new Optimizer().removeDuplicates(customSet).transform(input); + assertTrue(input.isEqualTo(output)); + assertSame(firstLower, output.getChildren().get(0)); + assertSame(firstLower, output.getChildren().get(1)); + } + + @Test + public void testFlattenChoiceNestedRight() { + Parser a = of('a'); + Parser b = of('b'); + Parser c = of('c'); + + // [a, [b, c]] + Parser input = a.or(b.or(c)); + Parser output = new Optimizer().flattenChoices().transform(input); + + assertTrue(output instanceof ChoiceParser); + assertEquals(3, output.getChildren().size()); + assertTrue(output.getChildren().get(0).isEqualTo(a)); + assertTrue(output.getChildren().get(1).isEqualTo(b)); + assertTrue(output.getChildren().get(2).isEqualTo(c)); + } + + @Test + public void testFlattenChoiceNestedLeft() { + Parser a = of('a'); + Parser b = of('b'); + Parser c = of('c'); + + // ChoiceParser([ChoiceParser([a, b]), c]) + Parser input = new ChoiceParser(new ChoiceParser(a, b), c); + Parser output = new Optimizer().flattenChoices().transform(input); + + assertTrue(output instanceof ChoiceParser); + assertEquals(3, output.getChildren().size()); + assertTrue(output.getChildren().get(0).isEqualTo(a)); + assertTrue(output.getChildren().get(1).isEqualTo(b)); + assertTrue(output.getChildren().get(2).isEqualTo(c)); + } + + @Test + public void testFlattenChoiceDeeplyNested() { + Parser a = of('a'); + Parser b = of('b'); + Parser c = of('c'); + Parser d = of('d'); + + // [a, [b, [c, d]]] + Parser input = a.or(b.or(c.or(d))); + Parser output = new Optimizer().flattenChoices().transform(input); + + assertTrue(output instanceof ChoiceParser); + assertEquals(4, output.getChildren().size()); + assertTrue(output.getChildren().get(0).isEqualTo(a)); + assertTrue(output.getChildren().get(1).isEqualTo(b)); + assertTrue(output.getChildren().get(2).isEqualTo(c)); + assertTrue(output.getChildren().get(3).isEqualTo(d)); + } + + @Test + public void testFlattenChoicePreservesFailureJoiner() { + FailureJoiner joiner = new FailureJoiner.SelectFarthest(); + Parser a = of('a'); + Parser b = of('b'); + Parser c = of('c'); + + Parser input = new ChoiceParser(joiner, a, new ChoiceParser(joiner, b, c)); + Parser output = new Optimizer().flattenChoices().transform(input); + + assertTrue(output instanceof ChoiceParser); + assertSame(joiner, ((ChoiceParser) output).getFailureJoiner()); + assertEquals(3, output.getChildren().size()); + } + + @Test + public void testFlattenChoiceAlreadyFlat() { + Parser input = new ChoiceParser(of('a'), of('b'), of('c')); + Parser output = new Optimizer().flattenChoices().transform(input); + + assertTrue(output.isEqualTo(input)); + assertEquals(3, output.getChildren().size()); + } + + @Test + public void testCharacterRepeaterStar() { + Parser input = of('a').star().flatten(); + Parser output = new Optimizer().characterRepeaters().transform(input); + + assertTrue(output instanceof RepeatingCharacterParser); + assertTrue(output.isEqualTo(of('a').starString())); + assertEquals("aaa", output.parse("aaa").get()); + assertTrue(output.accept("aaa")); + } + + @Test + public void testCharacterRepeaterPlus() { + Parser input = digit().plus().flatten(); + Parser output = new Optimizer().characterRepeaters().transform(input); + + assertTrue(output instanceof RepeatingCharacterParser); + assertTrue(output.isEqualTo(digit().plusString())); + assertEquals("123", output.parse("123").get()); + Result failure = output.parse("abc"); + assertTrue(failure.isFailure()); + } + + @Test + public void testCharacterRepeaterTimes() { + Parser input = lowerCase().repeat(3, 3).flatten(); + Parser output = new Optimizer().characterRepeaters().transform(input); + + assertTrue(output instanceof RepeatingCharacterParser); + assertTrue(output.isEqualTo(lowerCase().timesString(3))); + assertEquals("abc", output.parse("abc").get()); + assertTrue(output.parse("ab").isFailure()); + } + + @Test + public void testCharacterRepeaterCustomMessage() { + Parser input = of('a').plus().flatten("custom error message"); + Parser output = new Optimizer().optimizeCharacterRepeaters().transform(input); + + assertTrue(output instanceof RepeatingCharacterParser); + assertEquals("custom error message", ((RepeatingCharacterParser) output).getMessage()); + Result failure = output.parse("b"); + assertTrue(failure.isFailure()); + assertEquals("custom error message", failure.getMessage()); + } + + @Test + public void testCharacterRepeaterUnicode() { + Parser charParser = CharacterParser.any(true); + assertTrue(charParser instanceof UnicodeCharacterParser); + + Parser input = charParser.star().flatten(); + Parser output = new Optimizer().characterRepeaters().transform(input); + + assertTrue(output instanceof RepeatingCharacterParser); + RepeatingCharacterParser repeater = (RepeatingCharacterParser) output; + assertTrue(repeater.isUnicode()); + assertEquals("hello", output.parse("hello").get()); + } + + @Test + public void testCharacterRepeaterNonMatching() { + // Flatten over a sequence (not a repeating parser) + Parser input = of('a').seq(of('b')).flatten(); + Parser output = new Optimizer().characterRepeaters().transform(input); + assertTrue(output.isEqualTo(input)); + + // Flatten over possessive repeating of non-character parser + Parser nonCharRepeating = org.petitparser.parser.primitive.StringParser.of("ab").star().flatten(); + Parser output2 = new Optimizer().characterRepeaters().transform(nonCharRepeating); + assertTrue(output2.isEqualTo(nonCharRepeating)); + } + + @Test + public void testAllOptimizations() { + // A grammar with settable delegates, nested choices, and flatten repeating characters + SettableParser settable = SettableParser.undefined(); + Parser a = of('a'); + Parser b = of('b').star().flatten(); + Parser c = of('c'); + settable.set(b); + + Parser input = a.or(settable.or(c)); + Parser output = new Optimizer().all().transform(input); + + assertTrue(output instanceof ChoiceParser); + assertEquals(3, output.getChildren().size()); + assertTrue(output.getChildren().get(0).isEqualTo(a)); + assertTrue(output.getChildren().get(1) instanceof RepeatingCharacterParser); + assertTrue(output.getChildren().get(2).isEqualTo(c)); + } + + @Test + public void testStaticOptimize() { + Parser input = of('a').or(of('b').or(of('c'))); + Parser output = Optimizer.optimize(input); + + assertTrue(output instanceof ChoiceParser); + assertEquals(3, output.getChildren().size()); + } + + @Test + public void testAddGenericFunctionTransformer() { + Optimizer optimizer = new Optimizer(); + optimizer.add(p -> p instanceof CharacterParser ? of('z') : p); + + Parser input = of('a'); + Parser output = optimizer.transform(input); + assertTrue(output.isEqualTo(of('z'))); + } + + @Test + public void testAddRulesVarargsAndIterable() { + Optimizer optimizer = new Optimizer(); + optimizer.add(new RemoveDelegateRule(), new FlattenChoiceRule()); + assertEquals(2, optimizer.getRules().size()); + + Optimizer optimizer2 = new Optimizer(); + optimizer2.addAll(List.of(new CharacterRepeaterRule(), new RemoveDuplicateRule())); + assertEquals(2, optimizer2.getRules().size()); + } + + @Test + public void testFlattenChoiceDifferentFailureJoinerNotFlattened() { + FailureJoiner joinerLast = new FailureJoiner.SelectLast(); + FailureJoiner joinerFirst = new FailureJoiner.SelectFirst(); + + Parser a = of('a'); + Parser b = of('b'); + Parser c = of('c'); + + ChoiceParser inner = new ChoiceParser(joinerFirst, b, c); + ChoiceParser outer = new ChoiceParser(joinerLast, a, inner); + + // Parsing "x" on unoptimized: + // a fails at 0, inner joins b and c with SelectFirst -> b fails at 0 -> returns b. + // outer joins a and b with SelectLast -> returns b failure. + Result unoptResult = outer.parse("x"); + assertEquals("'b' expected", unoptResult.getMessage()); + + Parser optimized = new Optimizer().flattenChoices().transform(outer); + + // Should NOT flatten inner because joiners differ + assertTrue(optimized instanceof ChoiceParser); + assertEquals(2, optimized.getChildren().size()); + assertTrue(optimized.getChildren().get(1) instanceof ChoiceParser); + assertSame(joinerFirst, ((ChoiceParser) optimized.getChildren().get(1)).getFailureJoiner()); + + Result optResult = optimized.parse("x"); + assertEquals("'b' expected", optResult.getMessage()); + } + + @Test + public void testFlattenChoiceCycleSafe() { + ChoiceParser c1 = new ChoiceParser(of('a'), of('b')); + c1.replace(of('a'), c1); + c1.replace(of('b'), c1); + + FlattenChoiceRule rule = new FlattenChoiceRule(); + Parser result = rule.apply(c1); + assertSame(c1, result); + } + + @Test + public void testCharacterRepeaterWithSettableDelegate() { + SettableParser settable = SettableParser.with(of('a').star()); + Parser input = settable.flatten(); + + Parser output = new Optimizer().characterRepeaters().transform(input); + assertTrue(output instanceof RepeatingCharacterParser); + assertEquals("aaa", output.parse("aaa").get()); + } + + @Test + public void testRemoveDuplicatesResetAcrossRuns() { + Optimizer optimizer = new Optimizer().removeDuplicates(); + + Parser p1 = of('a').seq(of('a')); + Parser o1 = optimizer.transform(p1); + assertSame(o1.getChildren().get(0), o1.getChildren().get(1)); + + Parser p2 = of('a').seq(of('b')); + Parser o2 = optimizer.transform(p2); + // Node in o2 should NOT be contaminated with node from o1 + assertNotSame(o1.getChildren().get(0), o2.getChildren().get(0)); + } + + @Test + public void testOptimizerConstructors() { + Optimizer o1 = new Optimizer(new RemoveDelegateRule(), new FlattenChoiceRule()); + assertEquals(2, o1.getRules().size()); + + Optimizer o2 = new Optimizer(List.of(new CharacterRepeaterRule())); + assertEquals(1, o2.getRules().size()); + + Optimizer o3 = new Optimizer((Iterable) null); + assertEquals(0, o3.getRules().size()); + } + + @Test + public void testAllRulesConstant() { + assertEquals(4, Optimizer.ALL_RULES.size()); + assertTrue(Optimizer.ALL_RULES.get(0) instanceof RemoveDelegateRule); + assertTrue(Optimizer.ALL_RULES.get(1) instanceof FlattenChoiceRule); + assertTrue(Optimizer.ALL_RULES.get(2) instanceof CharacterRepeaterRule); + assertTrue(Optimizer.ALL_RULES.get(3) instanceof RemoveDuplicateRule); + } + + @Test(expected = NullPointerException.class) + public void testStaticOptimizeNull() { + Optimizer.optimize(null); + } + + @Test(expected = NullPointerException.class) + public void testTransformNull() { + new Optimizer().transform(null); + } + + @Test(expected = NullPointerException.class) + public void testAddNullRule() { + new Optimizer().add((OptimizeRule) null); + } + + @Test(expected = NullPointerException.class) + public void testOptimizeRuleOfNull() { + OptimizeRule.of(null); + } + + @Test(expected = NullPointerException.class) + public void testRemoveDuplicateRuleNullUniques() { + new RemoveDuplicateRule(null); + } + + @Test + public void testOptimizeRuleInterface() { + OptimizeRule rule = parser -> parser.settable(); + assertTrue(rule.getName().contains("Lambda")); + assertEquals("RemoveDelegateRule", new RemoveDelegateRule().getName()); + + Parser original = of('a'); + Parser optimized = rule.optimize(original); + assertTrue(optimized instanceof SettableParser); + + // Test OptimizeRule.of adaptation + OptimizeRule adapted = OptimizeRule.of(p -> of('z')); + assertTrue(adapted.apply(of('a')).isEqualTo(of('z'))); + assertSame(adapted, OptimizeRule.of(adapted)); + } + + @Test + public void testRemoveDelegateRuleDirect() { + RemoveDelegateRule rule = new RemoveDelegateRule(); + assertNull(rule.apply(null)); + + Parser target = of('a'); + SettableParser s1 = SettableParser.with(target); + SettableParser s2 = SettableParser.with(s1); + DelegateParser d1 = new DelegateParser(s2); + + assertSame(target, rule.apply(d1)); + assertSame(target, rule.apply(s2)); + assertSame(target, rule.apply(target)); + } + + @Test + public void testRemoveDelegateRuleCycle() { + RemoveDelegateRule rule = new RemoveDelegateRule(); + SettableParser s1 = SettableParser.undefined(); + SettableParser s2 = SettableParser.undefined(); + s1.set(s2); + s2.set(s1); + + // Should not infinite loop; returns one of the cycle nodes safely + Parser result = rule.apply(s1); + assertNotNull(result); + } + + @Test + public void testRemoveDuplicateRuleDirect() { + RemoveDuplicateRule rule = new RemoveDuplicateRule(); + assertNull(rule.apply(null)); + + Parser a1 = of('a'); + Parser a2 = of('a'); + assertNotEquals(System.identityHashCode(a1), System.identityHashCode(a2)); + + Parser first = rule.apply(a1); + assertSame(a1, first); + assertEquals(1, rule.getUniques().size()); + + Parser second = rule.apply(a2); + assertSame(a1, second); + + rule.clear(); + assertEquals(0, rule.getUniques().size()); + } + + @Test + public void testFlattenChoiceRuleDirect() { + FlattenChoiceRule rule = new FlattenChoiceRule(); + assertNull(rule.apply(null)); + + Parser nonChoice = of('a'); + assertSame(nonChoice, rule.apply(nonChoice)); + + Parser a = of('a'); + Parser b = of('b'); + Parser c = of('c'); + + ChoiceParser flatChoice = new ChoiceParser(a, b); + assertSame(flatChoice, rule.apply(flatChoice)); + + ChoiceParser nestedChoice = new ChoiceParser(a, new ChoiceParser(b, c)); + Parser flattened = rule.apply(nestedChoice); + assertTrue(flattened instanceof ChoiceParser); + assertEquals(3, flattened.getChildren().size()); + assertSame(a, flattened.getChildren().get(0)); + assertSame(b, flattened.getChildren().get(1)); + assertSame(c, flattened.getChildren().get(2)); + } + + @Test + public void testFlattenChoiceRuleWithJoiner() { + FlattenChoiceRule rule = new FlattenChoiceRule(); + FailureJoiner joiner = new FailureJoiner.SelectFirst(); + ChoiceParser nested = new ChoiceParser(joiner, of('a'), new ChoiceParser(joiner, of('b'), of('c'))); + ChoiceParser flattened = (ChoiceParser) rule.apply(nested); + + assertSame(joiner, flattened.getFailureJoiner()); + assertEquals(3, flattened.getChildren().size()); + } + + @Test + public void testCharacterRepeaterRuleDirect() { + CharacterRepeaterRule rule = new CharacterRepeaterRule(); + assertNull(rule.apply(null)); + + Parser nonFlatten = of('a').star(); + assertSame(nonFlatten, rule.apply(nonFlatten)); + + Parser seqFlatten = of('a').seq(of('b')).flatten(); + assertSame(seqFlatten, rule.apply(seqFlatten)); + + Parser nonCharFlatten = org.petitparser.parser.primitive.StringParser.of("xyz").star().flatten(); + assertSame(nonCharFlatten, rule.apply(nonCharFlatten)); + + // Normal CharacterParser + Parser charFlatten = of('a').star().flatten(); + Parser rewritten = rule.apply(charFlatten); + assertTrue(rewritten instanceof RepeatingCharacterParser); + RepeatingCharacterParser repeater = (RepeatingCharacterParser) rewritten; + assertEquals(0, repeater.getMin()); + assertEquals(-1, repeater.getMax()); + assertFalse(repeater.isUnicode()); + + // With custom message + Parser customMsg = of('b').plus().flatten("must have b's"); + RepeatingCharacterParser customRewritten = (RepeatingCharacterParser) rule.apply(customMsg); + assertEquals("must have b's", customRewritten.getMessage()); + assertEquals(1, customRewritten.getMin()); + + // Unicode + Parser unicodeChar = CharacterParser.any(true); + Parser unicodeFlatten = unicodeChar.times(2).flatten(); + RepeatingCharacterParser unicodeRewritten = (RepeatingCharacterParser) rule.apply(unicodeFlatten); + assertTrue(unicodeRewritten.isUnicode()); + assertEquals(2, unicodeRewritten.getMin()); + assertEquals(2, unicodeRewritten.getMax()); + } + + @Test + public void testFailureJoinerEqualsAndHashCode() { + FailureJoiner.SelectFirst sf1 = new FailureJoiner.SelectFirst(); + FailureJoiner.SelectFirst sf2 = new FailureJoiner.SelectFirst(); + assertEquals(sf1, sf2); + assertEquals(sf1.hashCode(), sf2.hashCode()); + assertNotEquals(sf1, new Object()); + + FailureJoiner.SelectLast sl1 = new FailureJoiner.SelectLast(); + FailureJoiner.SelectLast sl2 = new FailureJoiner.SelectLast(); + assertEquals(sl1, sl2); + assertEquals(sl1.hashCode(), sl2.hashCode()); + assertNotEquals(sl1, sf1); + + FailureJoiner.SelectFarthest sfarthest1 = new FailureJoiner.SelectFarthest(); + FailureJoiner.SelectFarthest sfarthest2 = new FailureJoiner.SelectFarthest(); + assertEquals(sfarthest1, sfarthest2); + assertEquals(sfarthest1.hashCode(), sfarthest2.hashCode()); + assertNotEquals(sfarthest1, sl1); + + FailureJoiner.SelectFarthestJoined sfj1 = new FailureJoiner.SelectFarthestJoined(" OR "); + FailureJoiner.SelectFarthestJoined sfj2 = new FailureJoiner.SelectFarthestJoined(" OR "); + FailureJoiner.SelectFarthestJoined sfj3 = new FailureJoiner.SelectFarthestJoined(" AND "); + assertEquals(sfj1, sfj2); + assertEquals(sfj1.hashCode(), sfj2.hashCode()); + assertNotEquals(sfj1, sfj3); + assertNotEquals(sfj1, sfarthest1); + } + + @Test + public void testChoiceParserHasEqualProperties() { + FailureJoiner joiner1 = new FailureJoiner.SelectLast(); + FailureJoiner joiner2 = new FailureJoiner.SelectLast(); + FailureJoiner joiner3 = new FailureJoiner.SelectFirst(); + + ChoiceParser c1 = new ChoiceParser(joiner1, of('a'), of('b')); + ChoiceParser c2 = new ChoiceParser(joiner2, of('a'), of('b')); + ChoiceParser c3 = new ChoiceParser(joiner3, of('a'), of('b')); + + assertTrue(c1.isEqualTo(c2)); + assertFalse(c1.isEqualTo(c3)); + } + + @Test + @SuppressWarnings({ "EqualsBetweenInconvertibleTypes" }) + public void testFailureJoinerEdgeCases() { + FailureJoiner.SelectFirst sf = new FailureJoiner.SelectFirst(); + assertTrue(sf.equals(sf)); + assertFalse(sf.equals(null)); + assertFalse(sf.equals((Object) "other")); + + FailureJoiner.SelectLast sl = new FailureJoiner.SelectLast(); + assertTrue(sl.equals(sl)); + assertFalse(sl.equals(null)); + assertFalse(sl.equals((Object) "other")); + + FailureJoiner.SelectFarthest sfarthest = new FailureJoiner.SelectFarthest(); + assertTrue(sfarthest.equals(sfarthest)); + assertFalse(sfarthest.equals(null)); + assertFalse(sfarthest.equals((Object) "other")); + + FailureJoiner.SelectFarthestJoined sfjDefault = new FailureJoiner.SelectFarthestJoined(); + FailureJoiner.SelectFarthestJoined sfjCustom = new FailureJoiner.SelectFarthestJoined(" OR "); + assertTrue(sfjDefault.equals(sfjDefault)); + assertTrue(sfjDefault.equals(sfjCustom)); + assertFalse(sfjDefault.equals(null)); + assertFalse(sfjDefault.equals((Object) "other")); + } + + @Test + public void testFlattenChoiceDifferentFailureJoinerDirect() { + FlattenChoiceRule rule = new FlattenChoiceRule(); + FailureJoiner joinerFirst = new FailureJoiner.SelectFirst(); + FailureJoiner joinerLast = new FailureJoiner.SelectLast(); + + ChoiceParser inner = new ChoiceParser(joinerFirst, of('b'), of('c')); + ChoiceParser outer = new ChoiceParser(joinerLast, of('a'), inner); + + Parser result = rule.apply(outer); + assertSame(outer, result); + assertEquals(2, ((ChoiceParser) result).getChildren().size()); + } + + @Test + public void testFlattenChoiceAllCyclesReturnsSame() { + FlattenChoiceRule rule = new FlattenChoiceRule(); + ChoiceParser c1 = new ChoiceParser(of('a'), of('b')); + c1.replace(of('a'), c1); + c1.replace(of('b'), c1); + + Parser result = rule.apply(c1); + assertSame(c1, result); + } + + @Test + public void testRemoveDuplicateResetClearsDefaultPreservesCustom() { + RemoveDuplicateRule defaultRule = new RemoveDuplicateRule(); + defaultRule.apply(of('a')); + assertEquals(1, defaultRule.getUniques().size()); + defaultRule.reset(); + assertEquals(0, defaultRule.getUniques().size()); + + Set customSet = new HashSet<>(); + customSet.add(of('a')); + RemoveDuplicateRule customRule = new RemoveDuplicateRule(customSet); + assertEquals(1, customRule.getUniques().size()); + customRule.reset(); + assertEquals(1, customRule.getUniques().size()); + } + + @Test + public void testCustomFlattenSubclassPreserved() { + CharacterRepeaterRule rule = new CharacterRepeaterRule(); + Parser custom = new org.petitparser.parser.actions.FlattenParser(of('a').star()) {}; + Parser rewritten = rule.apply(custom); + assertSame(custom, rewritten); + } + + @Test + public void testCustomChoiceSubclassPreserved() { + FlattenChoiceRule rule = new FlattenChoiceRule(); + ChoiceParser custom = new ChoiceParser(of('a'), of('b')) {}; + ChoiceParser outer = new ChoiceParser(of('c'), custom); + + Parser result = rule.apply(outer); + assertSame(outer, result); + } + + @Test + public void testFlattenChoiceCycle() { + FlattenChoiceRule rule = new FlattenChoiceRule(); + ChoiceParser c1 = new ChoiceParser(of('a'), of('b')); + c1.replace(of('b'), c1); + Parser result = rule.apply(c1); + assertNotNull(result); + } + + @Test + public void testCharacterRepeaterUnwrapsDelegates() { + CharacterRepeaterRule rule = new CharacterRepeaterRule(); + SettableParser s = SettableParser.with(of('a').star()); + Parser flatten = s.flatten(); + + Parser rewritten = rule.apply(flatten); + assertTrue(rewritten instanceof RepeatingCharacterParser); + assertEquals("aaa", rewritten.parse("aaa").get()); + } + + @Test + public void testCharacterRepeaterCycle() { + CharacterRepeaterRule rule = new CharacterRepeaterRule(); + + SettableParser s1 = SettableParser.undefined(); + SettableParser s2 = SettableParser.undefined(); + s1.set(s2); + s2.set(s1); + Parser flatten = s1.flatten(); + assertSame(flatten, rule.apply(flatten)); + } + + @Test + public void testRemoveDelegateCycle() { + RemoveDelegateRule rule = new RemoveDelegateRule(); + Parser a = of('a'); + Parser b = of('b'); + org.petitparser.parser.combinators.DelegateParser d1 = + new org.petitparser.parser.combinators.DelegateParser(a); + org.petitparser.parser.combinators.DelegateParser d2 = + new org.petitparser.parser.combinators.DelegateParser(b); + d1.replace(a, d2); + d2.replace(b, d1); + assertSame(d1, rule.apply(d1)); + } + + @Test + public void testOptimizerAddFunction() { + Optimizer opt = new Optimizer().add(p -> p); + assertNotNull(opt); + Parser parser = of('a'); + assertTrue(parser.isEqualTo(opt.transform(parser))); + } } diff --git a/petitparser-core/src/test/java/org/petitparser/utils/ProgressTest.java b/petitparser-core/src/test/java/org/petitparser/utils/ProgressTest.java new file mode 100644 index 0000000..5f82a97 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/utils/ProgressTest.java @@ -0,0 +1,339 @@ +package org.petitparser.utils; + +import org.junit.Test; +import org.petitparser.ExamplesTest; +import org.petitparser.context.Context; +import org.petitparser.context.Result; +import org.petitparser.parser.Parser; +import org.petitparser.parser.primitive.CharacterParser; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.letter; + +/** + * Tests {@link Progress}. + */ +public class ProgressTest { + + @Test + public void testSuccessfulProgress() { + List frames = new ArrayList<>(); + Result result = Progress.progress(ExamplesTest.IDENTIFIER, frames::add).parse("ab123"); + assertTrue(result.isSuccess()); + assertEquals("ab123", result.get()); + assertEquals(9, frames.size()); + + // Frame 0: FlattenParser at position 0 + assertEquals(0, frames.get(0).getPosition()); + assertEquals(-1, frames.get(0).getPreviousPosition()); + assertEquals(0, frames.get(0).getMaxPosition()); + assertFalse(frames.get(0).isBacktracking()); + assertFalse(frames.get(0).isBacktracked()); + assertTrue(frames.get(0).toString().startsWith("* FlattenParser")); + + // Frame 1: SequenceParser at position 0 + assertEquals(0, frames.get(1).getPosition()); + assertEquals(0, frames.get(1).getPreviousPosition()); + assertEquals(0, frames.get(1).getMaxPosition()); + assertFalse(frames.get(1).isBacktracking()); + assertTrue(frames.get(1).toString().startsWith("* SequenceParser")); + + // Frame 2: Sequence child 0 (letter) at position 0 + assertEquals(0, frames.get(2).getPosition()); + assertEquals(0, frames.get(2).getPreviousPosition()); + assertEquals(0, frames.get(2).getMaxPosition()); + assertFalse(frames.get(2).isBacktracking()); + assertTrue(frames.get(2).toString().startsWith("* CharacterParser")); + + // Frame 3: Sequence child 1 (repeating) at position 1 + assertEquals(1, frames.get(3).getPosition()); + assertEquals(0, frames.get(3).getPreviousPosition()); + assertEquals(1, frames.get(3).getMaxPosition()); + assertFalse(frames.get(3).isBacktracking()); + assertTrue(frames.get(3).toString().startsWith("** PossessiveRepeatingParser")); + + // Frame 4: repeating child at position 1 ('b') + assertEquals(1, frames.get(4).getPosition()); + assertEquals(1, frames.get(4).getPreviousPosition()); + assertEquals(1, frames.get(4).getMaxPosition()); + assertFalse(frames.get(4).isBacktracking()); + assertTrue(frames.get(4).toString().startsWith("** CharacterParser")); + + // Frame 5: repeating child at position 2 ('1') + assertEquals(2, frames.get(5).getPosition()); + assertEquals(1, frames.get(5).getPreviousPosition()); + assertEquals(2, frames.get(5).getMaxPosition()); + assertFalse(frames.get(5).isBacktracking()); + assertTrue(frames.get(5).toString().startsWith("*** CharacterParser")); + + // Frame 6: repeating child at position 3 ('2') + assertEquals(3, frames.get(6).getPosition()); + assertEquals(3, frames.get(6).getMaxPosition()); + assertFalse(frames.get(6).isBacktracking()); + assertTrue(frames.get(6).toString().startsWith("**** CharacterParser")); + + // Frame 7: repeating child at position 4 ('3') + assertEquals(4, frames.get(7).getPosition()); + assertEquals(4, frames.get(7).getMaxPosition()); + assertFalse(frames.get(7).isBacktracking()); + assertTrue(frames.get(7).toString().startsWith("***** CharacterParser")); + + // Frame 8: repeating child at position 5 (end of input) + assertEquals(5, frames.get(8).getPosition()); + assertEquals(5, frames.get(8).getMaxPosition()); + assertFalse(frames.get(8).isBacktracking()); + assertTrue(frames.get(8).toString().startsWith("****** CharacterParser")); + } + + @Test + public void testFailingProgress() { + List frames = new ArrayList<>(); + Result result = Progress.progress(ExamplesTest.IDENTIFIER, frames::add).parse("1"); + assertFalse(result.isSuccess()); + assertEquals(3, frames.size()); + + assertEquals(0, frames.get(0).getPosition()); + assertTrue(frames.get(0).toString().startsWith("* FlattenParser")); + + assertEquals(0, frames.get(1).getPosition()); + assertTrue(frames.get(1).toString().startsWith("* SequenceParser")); + + assertEquals(0, frames.get(2).getPosition()); + assertTrue(frames.get(2).toString().startsWith("* CharacterParser")); + } + + @Test + public void testProgressWithPredicate() { + List frames = new ArrayList<>(); + Parser parser = Progress.on( + ExamplesTest.IDENTIFIER, + p -> p instanceof CharacterParser, + frames::add + ); + Result result = parser.parse("ab123"); + assertTrue(result.isSuccess()); + // Only character parsers should be observed: 1 for letter + 5 for repeat child = 6 + assertEquals(6, frames.size()); + for (Progress.ProgressFrame frame : frames) { + assertTrue(frame.getParser() instanceof CharacterParser); + } + } + + @Test + public void testBacktrackingDetection() { + // Parser tries letter & digit, fails, backtracks to position 0, tries letter & letter + Parser branch1 = letter().seq(digit()); + Parser branch2 = letter().seq(letter()); + Parser choice = branch1.or(branch2); + + List frames = new ArrayList<>(); + Parser instrumented = Progress.progress(choice, frames::add); + Result result = instrumented.parse("ab"); + assertTrue(result.isSuccess()); + + List backtrackingFrames = frames.stream() + .filter(Progress.ProgressFrame::isBacktracking) + .collect(Collectors.toList()); + + assertEquals(1, backtrackingFrames.size()); + Progress.ProgressFrame btFrame = backtrackingFrames.get(0); + assertEquals(0, btFrame.getPosition()); + assertEquals(1, btFrame.getPreviousPosition()); + assertEquals(1, btFrame.getMaxPosition()); + assertTrue(btFrame.isBacktracking()); + assertTrue(btFrame.isBacktracked()); + assertTrue(btFrame.isBelowMaxPosition()); + } + + @Test + public void testMultipleRunsResetState() { + Parser branch1 = letter().seq(digit()); + Parser branch2 = letter().seq(letter()); + Parser choice = branch1.or(branch2); + + List firstRun = new ArrayList<>(); + Parser instrumented = Progress.progress(choice, firstRun::add); + instrumented.parse("ab"); + assertTrue(firstRun.stream().anyMatch(Progress.ProgressFrame::isBacktracking)); + + List secondRun = new ArrayList<>(); + Parser instrumented2 = Progress.progress(choice, secondRun::add); + instrumented2.parse("a1"); + // "a1" matches first branch, so it never backtracks + assertFalse(secondRun.stream().anyMatch(Progress.ProgressFrame::isBacktracking)); + } + + @Test + public void testDefaultAndFactoryOverloads() { + PrintStream originalOut = System.out; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(buffer)); + Parser parser = Progress.progress(letter()); + Result result = parser.parse("a"); + assertTrue(result.isSuccess()); + String output = buffer.toString(); + assertTrue(output.contains("* CharacterParser")); + } finally { + System.setOut(originalOut); + } + + // Test of() overloads + List framesOf = new ArrayList<>(); + Parser pOf = Progress.of(letter(), framesOf::add); + assertTrue(pOf.parse("a").isSuccess()); + assertEquals(1, framesOf.size()); + + List framesOfPred = new ArrayList<>(); + Parser pOfPred = Progress.of(letter(), p -> true, framesOfPred::add); + assertTrue(pOfPred.parse("a").isSuccess()); + assertEquals(1, framesOfPred.size()); + + // Test progress() without observer defaults to stdout + Parser pProgDefault = Progress.of(letter()); + assertNotNull(pProgDefault); + } + + @Test + public void testProgressFramePropertiesAndEquality() { + Context c0 = new Context("hello", 0); + Context c2 = new Context("hello", 2); + Parser p1 = letter(); + Parser p2 = digit(); + + Progress.ProgressFrame frame1 = new Progress.ProgressFrame(p1, c0); + Progress.ProgressFrame frame2 = new Progress.ProgressFrame(p1, c0); + Progress.ProgressFrame frame3 = new Progress.ProgressFrame(p1, c2, true); + Progress.ProgressFrame frame4 = new Progress.ProgressFrame(p2, c0); + + assertEquals(frame1, frame2); + assertEquals(frame1.hashCode(), frame2.hashCode()); + assertNotEquals(frame1, frame3); + assertNotEquals(frame1, frame4); + assertNotEquals(frame1, null); + assertNotEquals(frame1, "string"); + + assertEquals(p1, frame1.getParser()); + assertEquals(c0, frame1.getContext()); + assertEquals(0, frame1.getPosition()); + assertEquals(-1, frame1.getPreviousPosition()); + assertEquals(0, frame1.getMaxPosition()); + assertFalse(frame1.isBacktracking()); + assertFalse(frame1.isBacktracked()); + + assertEquals(2, frame3.getPosition()); + assertTrue(frame3.isBacktracking()); + + // Test negative position clamp in toString + Context negContext = new Context("x", -1); + Progress.ProgressFrame negFrame = new Progress.ProgressFrame(p1, negContext, -1, -1, -1, false); + assertEquals("* " + p1, negFrame.toString()); + } + + @Test(expected = NullPointerException.class) + public void testNullRootThrows() { + Progress.on(null, frame -> {}); + } + + @Test(expected = NullPointerException.class) + public void testNullPredicateThrows() { + Progress.on(letter(), null, frame -> {}); + } + + @Test(expected = NullPointerException.class) + public void testNullObserverThrows() { + Progress.on(letter(), (Consumer) null); + } + + @Test + public void testConcurrentProgressParsing() throws InterruptedException { + int threadCount = 8; + int iterationsPerThread = 50; + Parser choice = letter().seq(digit()).or(letter().seq(letter())); + // Shared progress parser across threads + ThreadLocal> threadFrames = ThreadLocal.withInitial(ArrayList::new); + Parser instrumented = Progress.progress(choice, frame -> threadFrames.get().add(frame)); + + Thread[] threads = new Thread[threadCount]; + boolean[] failures = new boolean[threadCount]; + for (int i = 0; i < threadCount; i++) { + final int threadIndex = i; + threads[i] = new Thread(() -> { + try { + for (int iter = 0; iter < iterationsPerThread; iter++) { + threadFrames.get().clear(); + Result res = instrumented.parse("ab"); + if (!res.isSuccess()) { + failures[threadIndex] = true; + return; + } + List frames = threadFrames.get(); + // Verify backtracking detection is accurate and not clobbered + long btCount = frames.stream().filter(Progress.ProgressFrame::isBacktracking).count(); + if (btCount != 1) { + failures[threadIndex] = true; + return; + } + } + } catch (Throwable t) { + failures[threadIndex] = true; + } + }); + } + + for (Thread t : threads) { + t.start(); + } + for (Thread t : threads) { + t.join(); + } + + for (int i = 0; i < threadCount; i++) { + assertFalse("Thread " + i + " experienced concurrency failure", failures[i]); + } + } + + @Test + public void testReentrantProgressParsing() { + List outerFrames = new ArrayList<>(); + List innerFrames = new ArrayList<>(); + + Parser innerParser = Progress.progress(digit().plus(), innerFrames::add); + // Outer parser maps a letter and executes the inner parser within an action + Parser outerParser = letter().map(ch -> innerParser.parse("123").get()); + Parser instrumentedOuter = Progress.progress(outerParser, outerFrames::add); + + Result result = instrumentedOuter.parse("a"); + assertTrue(result.isSuccess()); + assertFalse(outerFrames.isEmpty()); + assertFalse(innerFrames.isEmpty()); + // Ensure both ran cleanly and outer state wasn't nullified prematurely + assertFalse(outerFrames.stream().anyMatch(Progress.ProgressFrame::isBacktracking)); + assertFalse(innerFrames.stream().anyMatch(Progress.ProgressFrame::isBacktracking)); + } + + @Test + public void testDirectInnerInvocationFallback() { + List frames = new ArrayList<>(); + Parser root = digit(); + Parser instrumented = Progress.on(root, p -> true, frames::add); + // The instrumented parser wraps the transformed parser + Parser inner = instrumented.getChildren().get(0); + Result result = inner.parseOn(new Context("1", 0)); + assertTrue(result.isSuccess()); + assertEquals(1, frames.size()); + assertEquals(0, frames.get(0).getPosition()); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/utils/functions/FunctionsTest.java b/petitparser-core/src/test/java/org/petitparser/utils/functions/FunctionsTest.java new file mode 100644 index 0000000..f975c7f --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/utils/functions/FunctionsTest.java @@ -0,0 +1,112 @@ +package org.petitparser.utils.functions; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Tests {@link Function3} through {@link Function9}. + */ +public class FunctionsTest { + + @Test + public void testFunction3() { + Function3 f = (a, b, c) -> a + b + c; + assertEquals("abc", f.apply("a", "b", "c")); + assertEquals("ABC", f.andThen(String::toUpperCase).apply("a", "b", "c")); + } + + @Test(expected = NullPointerException.class) + public void testFunction3NullAfter() { + Function3 f = (a, b, c) -> a + b + c; + f.andThen(null); + } + + @Test + public void testFunction4() { + Function4 f = (a, b, c, d) -> a + b + c + d; + assertEquals("abcd", f.apply("a", "b", "c", "d")); + assertEquals("ABCD", f.andThen(String::toUpperCase).apply("a", "b", "c", "d")); + } + + @Test(expected = NullPointerException.class) + public void testFunction4NullAfter() { + Function4 f = (a, b, c, d) -> a + b + c + d; + f.andThen(null); + } + + @Test + public void testFunction5() { + Function5 f = + (a, b, c, d, e) -> a + b + c + d + e; + assertEquals("abcde", f.apply("a", "b", "c", "d", "e")); + assertEquals("ABCDE", f.andThen(String::toUpperCase).apply("a", "b", "c", "d", "e")); + } + + @Test(expected = NullPointerException.class) + public void testFunction5NullAfter() { + Function5 f = + (a, b, c, d, e) -> a + b + c + d + e; + f.andThen(null); + } + + @Test + public void testFunction6() { + Function6 f = + (a, b, c, d, e, g) -> a + b + c + d + e + g; + assertEquals("abcdef", f.apply("a", "b", "c", "d", "e", "f")); + assertEquals("ABCDEF", f.andThen(String::toUpperCase).apply("a", "b", "c", "d", "e", "f")); + } + + @Test(expected = NullPointerException.class) + public void testFunction6NullAfter() { + Function6 f = + (a, b, c, d, e, g) -> a + b + c + d + e + g; + f.andThen(null); + } + + @Test + public void testFunction7() { + Function7 f = + (a, b, c, d, e, g, h) -> a + b + c + d + e + g + h; + assertEquals("abcdefg", f.apply("a", "b", "c", "d", "e", "f", "g")); + assertEquals("ABCDEFG", f.andThen(String::toUpperCase).apply("a", "b", "c", "d", "e", "f", "g")); + } + + @Test(expected = NullPointerException.class) + public void testFunction7NullAfter() { + Function7 f = + (a, b, c, d, e, g, h) -> a + b + c + d + e + g + h; + f.andThen(null); + } + + @Test + public void testFunction8() { + Function8 f = + (a, b, c, d, e, g, h, i) -> a + b + c + d + e + g + h + i; + assertEquals("abcdefgh", f.apply("a", "b", "c", "d", "e", "f", "g", "h")); + assertEquals("ABCDEFGH", f.andThen(String::toUpperCase).apply("a", "b", "c", "d", "e", "f", "g", "h")); + } + + @Test(expected = NullPointerException.class) + public void testFunction8NullAfter() { + Function8 f = + (a, b, c, d, e, g, h, i) -> a + b + c + d + e + g + h + i; + f.andThen(null); + } + + @Test + public void testFunction9() { + Function9 f = + (a, b, c, d, e, g, h, i, j) -> a + b + c + d + e + g + h + i + j; + assertEquals("abcdefghi", f.apply("a", "b", "c", "d", "e", "f", "g", "h", "i")); + assertEquals("ABCDEFGHI", f.andThen(String::toUpperCase).apply("a", "b", "c", "d", "e", "f", "g", "h", "i")); + } + + @Test(expected = NullPointerException.class) + public void testFunction9NullAfter() { + Function9 f = + (a, b, c, d, e, g, h, i, j) -> a + b + c + d + e + g + h + i + j; + f.andThen(null); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/utils/linter/LinterRulesTest.java b/petitparser-core/src/test/java/org/petitparser/utils/linter/LinterRulesTest.java new file mode 100644 index 0000000..9be7dc4 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/utils/linter/LinterRulesTest.java @@ -0,0 +1,704 @@ +package org.petitparser.utils.linter; + +import org.junit.Test; +import org.petitparser.parser.Parser; +import org.petitparser.parser.combinators.ChoiceParser; +import org.petitparser.parser.combinators.SettableParser; +import org.petitparser.utils.Analyzer; +import org.petitparser.utils.Linter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.petitparser.parser.primitive.CharacterParser.any; +import static org.petitparser.parser.primitive.CharacterParser.digit; +import static org.petitparser.parser.primitive.CharacterParser.newline; +import static org.petitparser.parser.primitive.CharacterParser.of; +import static org.petitparser.parser.primitive.EpsilonParser.epsilon; +import static org.petitparser.parser.primitive.StringParser.of; + +/** + * Unit tests for {@link Linter} and all 13 {@link LinterRule} implementations. + */ +public class LinterRulesTest { + + // -------------------------------------------------------------------------- + // Architecture Tests + // -------------------------------------------------------------------------- + + @Test + public void testLinterTypeEnum() { + assertEquals(3, LinterType.values().length); + assertEquals(LinterType.INFO, LinterType.valueOf("INFO")); + assertEquals(LinterType.WARNING, LinterType.valueOf("WARNING")); + assertEquals(LinterType.ERROR, LinterType.valueOf("ERROR")); + } + + @Test + public void testLinterRuleBase() { + AtomicBoolean called = new AtomicBoolean(false); + LinterRule rule = LinterRule.of(LinterType.WARNING, "Custom", (r, a, p, cb) -> { + called.set(true); + cb.accept(new LinterIssue(r, p, "Custom issue")); + }); + + assertEquals(LinterType.WARNING, rule.getType()); + assertEquals("Custom", rule.getTitle()); + assertTrue(rule.toString().contains("Custom")); + + Parser parser = of('a'); + List issues = Linter.lint(parser, List.of(rule)); + assertTrue(called.get()); + assertEquals(1, issues.size()); + assertEquals("Custom issue", issues.get(0).getDescription()); + assertEquals(LinterType.WARNING, issues.get(0).getType()); + assertEquals("Custom", issues.get(0).getTitle()); + assertSame(parser, issues.get(0).getParser()); + assertSame(rule, issues.get(0).getRule()); + + // Equality and hashCode of LinterRule + LinterRule sameRule = LinterRule.of(LinterType.WARNING, "Custom", (r, a, p, cb) -> {}); + assertEquals(rule, sameRule); + assertNotEquals(rule, LinterRule.of(LinterType.ERROR, "Custom", (r, a, p, cb) -> {})); + assertNotEquals(rule, LinterRule.of(LinterType.WARNING, "Other", (r, a, p, cb) -> {})); + assertEquals(new CharacterRepeaterRule(), new CharacterRepeaterRule()); + assertEquals(new CharacterRepeaterRule().hashCode(), new CharacterRepeaterRule().hashCode()); + assertNotEquals(new CharacterRepeaterRule(), new DuplicateParserRule()); + assertNotEquals(new CharacterRepeaterRule(), null); + assertNotEquals(new CharacterRepeaterRule(), "other"); + } + + @Test + public void testLinterIssueEqualityAndToString() { + LinterRule rule1 = new CharacterRepeaterRule(); + LinterRule rule2 = new DuplicateParserRule(); + Parser p1 = of('a'); + Parser p2 = of('b'); + + LinterIssue issue1 = new LinterIssue(rule1, p1, "Desc"); + LinterIssue issue1Same = new LinterIssue(rule1, p1, "Desc"); + LinterIssue issue2 = new LinterIssue(rule2, p1, "Desc"); + LinterIssue issue3 = new LinterIssue(rule1, p2, "Desc"); + LinterIssue issue4 = new LinterIssue(rule1, p1, "Other"); + + assertEquals(issue1, issue1); + assertEquals(issue1, issue1Same); + assertEquals(issue1.hashCode(), issue1Same.hashCode()); + assertNotEquals(issue1, issue2); + assertNotEquals(issue1, issue3); + assertNotEquals(issue1, issue4); + assertNotEquals(issue1, null); + assertNotEquals(issue1, "string"); + + assertTrue(issue1.toString().contains("LinterIssue")); + assertTrue(issue1.toString().contains("Character repeater")); + assertTrue(issue1.toString().contains("Desc")); + } + + @Test + public void testLinterFormatIterable() { + List items = List.of("alpha", "beta"); + assertEquals(" - alpha\n - beta", LinterRule.formatIterable(items)); + assertEquals(" 1: alpha\n 2: beta", LinterRule.formatIterable(items, 1)); + assertEquals("", LinterRule.formatIterable(Collections.emptyList())); + } + + @Test + public void testLinterIsParserIterableEqual() { + Parser a1 = of('a'); + Parser a2 = of('a'); + Parser b1 = of('b'); + Parser b2 = of('b'); + + assertTrue(LinterRule.isParserIterableEqual(List.of(a1, b1), List.of(b2, a2))); + assertFalse(LinterRule.isParserIterableEqual(List.of(a1), List.of(a1, b1))); + assertFalse(LinterRule.isParserIterableEqual(List.of(a1, b1), List.of(a1))); + assertFalse(LinterRule.isParserIterableEqual(List.of(a1), List.of(b1))); + } + + @Test + public void testLinterEngineInvocationAndCallback() { + Parser input = of('a').or(of('b')); + Set seen = new HashSet<>(); + LinterRule fakeRule = LinterRule.of(LinterType.ERROR, "Fake Rule", (rule, analyzer, parser, cb) -> { + seen.add(parser); + }); + + List results = Linter.lint(input, List.of(fakeRule)); + assertTrue(results.isEmpty()); + assertEquals(3, seen.size()); + assertTrue(seen.contains(input)); + assertTrue(seen.contains(input.getChildren().get(0))); + assertTrue(seen.contains(input.getChildren().get(1))); + + List callbackIssues = new ArrayList<>(); + LinterRule triggerRule = LinterRule.of(LinterType.ERROR, "Trigger Rule", (rule, analyzer, parser, cb) -> { + if (parser == input) { + cb.accept(new LinterIssue(rule, parser, "Triggered")); + } + }); + + results = Linter.lint(input, callbackIssues::add, List.of(triggerRule)); + assertEquals(1, results.size()); + assertEquals(results, callbackIssues); + } + + @Test + public void testLinterDefaultRulesExclusions() { + // Info rule issues are excluded by default in Linter.lint(parser) + Parser input = of('a').or(of('b').or(of('c'))); // Nested choice is INFO + List issues = Linter.lint(input); + assertTrue(issues.isEmpty()); // NestedChoice is INFO, excluded by default + + // If we include all types via Query: + issues = Linter.query(input).includeAllTypes().lint(); + assertFalse(issues.isEmpty()); + assertEquals("Nested choice", issues.get(0).getTitle()); + + // Exclude specific rule title + issues = Linter.query(input).includeAllTypes().excludeRule("Nested choice").lint(); + assertTrue(issues.isEmpty()); + + // Exclude specific types + issues = Linter.query(input).includeAllTypes().excludeType(LinterType.INFO).lint(); + assertTrue(issues.isEmpty()); + + // Facade org.petitparser.utils.Linter + assertEquals(0, org.petitparser.utils.Linter.lint(input).size()); + assertNotNull(org.petitparser.utils.Linter.query(input)); + } + + @Test + public void testLinterQueryBuilder() { + Parser input = of('a'); + List issues = Linter.query(input) + .rules(new CharacterRepeaterRule(), new DuplicateParserRule()) + .excludeRules(List.of("Duplicate parser")) + .excludeTypes(List.of(LinterType.INFO)) + .onIssue(issue -> {}) + .lint(); + assertTrue(issues.isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 1: CharacterRepeaterRule + // -------------------------------------------------------------------------- + + @Test + public void testCharacterRepeaterRulePositive() { + List rules = List.of(new CharacterRepeaterRule()); + + Parser p1 = of('a').star().flatten(); + List issues1 = Linter.lint(p1, rules); + assertEquals(1, issues1.size()); + assertSame(p1, issues1.get(0).getParser()); + assertEquals(LinterType.WARNING, issues1.get(0).getType()); + assertEquals("Character repeater", issues1.get(0).getTitle()); + assertTrue(issues1.get(0).getDescription().contains("starString")); + + Parser p2 = any().plus().flatten(); + List issues2 = Linter.lint(p2, rules); + assertEquals(1, issues2.size()); + assertSame(p2, issues2.get(0).getParser()); + assertEquals("Character repeater", issues2.get(0).getTitle()); + } + + @Test + public void testCharacterRepeaterRuleNegative() { + List rules = List.of(new CharacterRepeaterRule()); + + Parser p1 = of('a').plus().token(); + assertTrue(Linter.lint(p1, rules).isEmpty()); + + Parser p2 = of("abc").star().flatten(); + assertTrue(Linter.lint(p2, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 2: DuplicateParserRule + // -------------------------------------------------------------------------- + + @Test + public void testDuplicateParserRulePositive() { + List rules = List.of(new DuplicateParserRule()); + + Parser p = digit().seq(digit()); + List issues = Linter.lint(p, rules); + assertEquals(1, issues.size()); + assertSame(p.getChildren().get(0), issues.get(0).getParser()); + assertEquals(LinterType.INFO, issues.get(0).getType()); + assertEquals("Duplicate parser", issues.get(0).getTitle()); + assertTrue(issues.get(0).getDescription().contains("2 instances")); + } + + @Test + public void testDuplicateParserRuleNegative() { + List rules = List.of(new DuplicateParserRule()); + + Parser p = digit("first").seq(digit("second")); + assertTrue(Linter.lint(p, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 3: LeftRecursionRule + // -------------------------------------------------------------------------- + + @Test + public void testLeftRecursionRulePositive() { + List rules = List.of(new LeftRecursionRule()); + + SettableParser s = SettableParser.undefined(); + s.set(s); + List issues = Linter.lint(s, rules); + assertEquals(1, issues.size()); + assertSame(s, issues.get(0).getParser()); + assertEquals(LinterType.ERROR, issues.get(0).getType()); + assertEquals("Left recursion", issues.get(0).getTitle()); + assertTrue(issues.get(0).getDescription().contains("without consuming input")); + } + + @Test + public void testLeftRecursionRuleNegative() { + List rules = List.of(new LeftRecursionRule()); + + Parser p = digit(); + assertTrue(Linter.lint(p, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 4: NestedChoiceRule + // -------------------------------------------------------------------------- + + @Test + public void testNestedChoiceRulePositive() { + List rules = List.of(new NestedChoiceRule()); + + Parser nested = of('2').or(of('3')); + Parser p = new ChoiceParser(of('1'), nested, of('4')); + List issues = Linter.lint(p, rules); + assertEquals(1, issues.size()); + assertSame(p, issues.get(0).getParser()); + assertEquals(LinterType.INFO, issues.get(0).getType()); + assertEquals("Nested choice", issues.get(0).getTitle()); + assertTrue(issues.get(0).getDescription().contains("index 1")); + } + + @Test + public void testNestedChoiceRuleNegative() { + List rules = List.of(new NestedChoiceRule()); + + Parser p1 = Parser.or(of('1'), of('2').or(of('3')).flatten(), of('4')); + assertTrue(Linter.lint(p1, rules).isEmpty()); + + Parser p2 = Parser.or(of('1'), of('2'), of('3'), of('4')); + assertTrue(Linter.lint(p2, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 5: NullableRepeaterRule + // -------------------------------------------------------------------------- + + @Test + public void testNullableRepeaterRulePositive() { + List rules = List.of(new NullableRepeaterRule()); + + Parser p1 = epsilon().star().optional(); + List issues1 = Linter.lint(p1, rules); + assertEquals(1, issues1.size()); + assertSame(p1.getChildren().get(0), issues1.get(0).getParser()); + assertEquals(LinterType.ERROR, issues1.get(0).getType()); + assertEquals("Nullable repeater", issues1.get(0).getTitle()); + assertTrue(issues1.get(0).getDescription().contains("infinite loop")); + + // Separated repeating parser with both delegate and sep nullable + Parser p2 = epsilon().starSeparated(epsilon()); + List issues2 = Linter.lint(p2, rules); + assertEquals(1, issues2.size()); + assertEquals("Nullable repeater", issues2.get(0).getTitle()); + } + + @Test + public void testNullableRepeaterRuleNegative() { + List rules = List.of(new NullableRepeaterRule()); + + Parser p1 = digit().star().optional(); + assertTrue(Linter.lint(p1, rules).isEmpty()); + + // Separated repeating with non-nullable delegate or non-nullable sep + assertTrue(Linter.lint(epsilon().starSeparated(any()), rules).isEmpty()); + assertTrue(Linter.lint(any().starSeparated(epsilon()), rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 6: OverlappingChoiceRule + // -------------------------------------------------------------------------- + + @Test + public void testOverlappingChoiceRulePositive() { + List rules = List.of(new OverlappingChoiceRule()); + + Parser p = Parser.or( + of('1'), + of('2').seq(of('a')), + of('2').seq(of('b')), + of('3') + ); + List issues = Linter.lint(p, rules); + assertEquals(1, issues.size()); + assertSame(p, issues.get(0).getParser()); + assertEquals(LinterType.INFO, issues.get(0).getType()); + assertEquals("Overlapping choice", issues.get(0).getTitle()); + assertTrue(issues.get(0).getDescription().contains("overlapping first-sets")); + } + + @Test + public void testOverlappingChoiceRuleNegative() { + List rules = List.of(new OverlappingChoiceRule()); + + Parser p = Parser.or(of('1'), of('2'), of('3')); + assertTrue(Linter.lint(p, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 7: RepeatedChoiceRule + // -------------------------------------------------------------------------- + + @Test + public void testRepeatedChoiceRulePositive() { + List rules = List.of(new RepeatedChoiceRule()); + + Parser p = new ChoiceParser(of('1'), of('2'), of('3'), of('2'), of('4')); + List issues = Linter.lint(p, rules); + assertEquals(1, issues.size()); + assertSame(p, issues.get(0).getParser()); + assertEquals(LinterType.WARNING, issues.get(0).getType()); + assertEquals("Repeated choice", issues.get(0).getTitle()); + assertTrue(issues.get(0).getDescription().contains("index 1 and 3 are identical")); + } + + @Test + public void testRepeatedChoiceRuleNegative() { + List rules = List.of(new RepeatedChoiceRule()); + + Parser p = Parser.or(of('1'), of('2'), of('3'), of('4')); + assertTrue(Linter.lint(p, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 8: UnnecessaryFlattenRule + // -------------------------------------------------------------------------- + + @Test + public void testUnnecessaryFlattenRulePositive() { + List rules = List.of(new UnnecessaryFlattenRule()); + + Parser p1 = any().flatten(); + List issues1 = Linter.lint(p1, rules); + assertEquals(1, issues1.size()); + assertSame(p1, issues1.get(0).getParser()); + assertEquals(LinterType.WARNING, issues1.get(0).getType()); + assertEquals("Unnecessary flatten", issues1.get(0).getTitle()); + assertTrue(issues1.get(0).getDescription().contains("adds unnecessary overhead")); + + Parser p2 = of("hello").flatten(); + assertEquals(1, Linter.lint(p2, rules).size()); + + Parser p3 = newline().flatten(); + assertEquals(1, Linter.lint(p3, rules).size()); + + Parser p4 = digit().starString().flatten(); + assertEquals(1, Linter.lint(p4, rules).size()); + + Parser p5 = any().flatten().flatten(); + assertEquals(2, Linter.lint(p5, rules).size()); + } + + @Test + public void testUnnecessaryFlattenRuleNegative() { + List rules = List.of(new UnnecessaryFlattenRule()); + + Parser p1 = any().optional().flatten(); + assertTrue(Linter.lint(p1, rules).isEmpty()); + + Parser p2 = any().flatten("message"); + assertTrue(Linter.lint(p2, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 9: UnnecessaryResolvableRule + // -------------------------------------------------------------------------- + + @Test + public void testUnnecessaryResolvableRulePositive() { + List rules = List.of(new UnnecessaryResolvableRule()); + + SettableParser s = of('a').settable(); + List issues = Linter.lint(s, rules); + assertEquals(1, issues.size()); + assertSame(s, issues.get(0).getParser()); + assertEquals(LinterType.WARNING, issues.get(0).getType()); + assertEquals("Unnecessary resolvable", issues.get(0).getTitle()); + assertTrue(issues.get(0).getDescription().contains("resolve(parser)")); + } + + @Test + public void testUnnecessaryResolvableRuleNegative() { + List rules = List.of(new UnnecessaryResolvableRule()); + + Parser p = of('a'); + assertTrue(Linter.lint(p, rules).isEmpty()); + + Parser resolved = Analyzer.resolve(of('a').settable()); + assertTrue(Linter.lint(resolved, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 10: UnoptimizedFlattenRule + // -------------------------------------------------------------------------- + + @Test + public void testUnoptimizedFlattenRulePositive() { + List rules = List.of(new UnoptimizedFlattenRule()); + + Parser p = any().flatten(); + List issues = Linter.lint(p, rules); + assertEquals(1, issues.size()); + assertSame(p, issues.get(0).getParser()); + assertEquals(LinterType.INFO, issues.get(0).getType()); + assertEquals("Unoptimized flatten", issues.get(0).getTitle()); + assertTrue(issues.get(0).getDescription().contains("fast parsing mode")); + } + + @Test + public void testUnoptimizedFlattenRuleNegative() { + List rules = List.of(new UnoptimizedFlattenRule()); + + Parser p = any().flatten("anything really"); + assertTrue(Linter.lint(p, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 11: UnreachableChoiceRule + // -------------------------------------------------------------------------- + + @Test + public void testUnreachableChoiceRulePositive() { + List rules = List.of(new UnreachableChoiceRule()); + + Parser p = new ChoiceParser(of('1'), of('2'), epsilon(), of('3')); + List issues = Linter.lint(p, rules); + assertEquals(1, issues.size()); + assertSame(p, issues.get(0).getParser()); + assertEquals(LinterType.WARNING, issues.get(0).getType()); + assertEquals("Unreachable choice", issues.get(0).getTitle()); + assertTrue(issues.get(0).getDescription().contains("index 2 is nullable")); + } + + @Test + public void testUnreachableChoiceRuleNegative() { + List rules = List.of(new UnreachableChoiceRule()); + + Parser p1 = Parser.or(of('1'), of('2'), of('3')); + assertTrue(Linter.lint(p1, rules).isEmpty()); + + // Nullable choice as the last option is reachable and fine + Parser p2 = Parser.or(of('1'), of('2'), epsilon()); + assertTrue(Linter.lint(p2, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 12: UnresolvedSettableRule + // -------------------------------------------------------------------------- + + @Test + public void testUnresolvedSettableRulePositive() { + List rules = List.of(new UnresolvedSettableRule()); + + SettableParser s = SettableParser.undefined(); + List issues = Linter.lint(s, rules); + assertEquals(1, issues.size()); + assertSame(s, issues.get(0).getParser()); + assertEquals(LinterType.ERROR, issues.get(0).getType()); + assertEquals("Unresolved settable", issues.get(0).getTitle()); + assertTrue(issues.get(0).getDescription().contains("undefined()")); + } + + @Test + public void testUnresolvedSettableRuleNegative() { + List rules = List.of(new UnresolvedSettableRule()); + + Parser p1 = digit().settable(); + assertTrue(Linter.lint(p1, rules).isEmpty()); + + SettableParser s2 = SettableParser.undefined(); + s2.set(digit()); + assertTrue(Linter.lint(s2, rules).isEmpty()); + } + + // -------------------------------------------------------------------------- + // Rule 13: UnusedResultRule + // -------------------------------------------------------------------------- + + @Test + public void testUnusedResultRulePositive() { + List rules = List.of(new UnusedResultRule()); + + Parser p1 = digit().map((Character c) -> Character.getNumericValue(c)).star().flatten(); + List issues1 = Linter.lint(p1, rules); + assertEquals(1, issues1.size()); + assertSame(p1, issues1.get(0).getParser()); + assertEquals(LinterType.INFO, issues1.get(0).getType()); + assertEquals("Unused result", issues1.get(0).getTitle()); + assertTrue(issues1.get(0).getDescription().contains("discards the result")); + + Parser p2 = digit().token().flatten(); + assertEquals(1, Linter.lint(p2, rules).size()); + + Parser p3 = digit().cast().flatten(); + assertEquals(1, Linter.lint(p3, rules).size()); + + Parser p4 = digit().where(c -> true).flatten(); + assertEquals(1, Linter.lint(p4, rules).size()); + + Parser p5 = digit().seq(digit()).pick(0).flatten(); + assertEquals(1, Linter.lint(p5, rules).size()); + + Parser p6 = digit().seq(digit()).permute(1, 0).flatten(); + assertEquals(1, Linter.lint(p6, rules).size()); + } + + @Test + public void testUnusedResultRuleNegative() { + List rules = List.of(new UnusedResultRule()); + + Parser p1 = digit().star().flatten(); + assertTrue(Linter.lint(p1, rules).isEmpty()); + + Parser p2 = digit().mapWithSideEffects(x -> x).flatten(); + assertTrue(Linter.lint(p2, rules).isEmpty()); + } + + + // -------------------------------------------------------------------------- + // Engine Constants and Edge Cases + // -------------------------------------------------------------------------- + + @Test + public void testEngineConstants() { + assertEquals(13, Linter.ALL_RULES.size()); + Set titles = new HashSet<>(); + for (LinterRule rule : Linter.ALL_RULES) { + assertNotNull(rule.getTitle()); + assertNotNull(rule.getType()); + assertTrue(titles.add(rule.getTitle())); + assertEquals(rule, rule); + assertNotEquals(rule, null); + assertNotEquals(rule, new Object()); + assertTrue(rule.toString().contains(rule.getTitle())); + } + assertEquals(Set.of(LinterType.INFO), Linter.DEFAULT_EXCLUDED_TYPES); + } + + @Test(expected = UnsupportedOperationException.class) + public void testAllRulesUnmodifiable() { + Linter.ALL_RULES.add(new CharacterRepeaterRule()); + } + + @Test + public void testLintVarargsAndCallbackMethods() { + Parser p = of('a').star().flatten(); + List issues = Linter.lint(p, new CharacterRepeaterRule(), new DuplicateParserRule()); + assertEquals(1, issues.size()); + assertEquals("Character repeater", issues.get(0).getTitle()); + + List collected = new ArrayList<>(); + issues = Linter.lint(p, collected::add); + assertEquals(1, issues.size()); + assertEquals(issues, collected); + + Parser pSafe = of('a'); + collected.clear(); + issues = Linter.lint(pSafe, collected::add); + assertTrue(issues.isEmpty()); + assertTrue(collected.isEmpty()); + } + + @Test + public void testUnusedResultRuleWithConstantAndCastList() { + List rules = List.of(new UnusedResultRule()); + Parser p1 = digit().constant(42).flatten(); + assertEquals(1, Linter.lint(p1, rules).size()); + + Parser p2 = digit().castList(String.class).flatten(); + assertEquals(1, Linter.lint(p2, rules).size()); + } + + @Test(expected = NullPointerException.class) + public void testLintNullParser() { + Linter.lint(null); + } + + @Test(expected = NullPointerException.class) + public void testQueryNullParser() { + Linter.query(null); + } + + @Test(expected = NullPointerException.class) + public void testLinterRuleOfNullFunction() { + LinterRule.of(LinterType.INFO, "Title", null); + } + + @Test(expected = NullPointerException.class) + public void testLinterRuleNullType() { + new CharacterRepeaterRule() { + { + LinterRule.of(null, "Title", (r, a, p, cb) -> {}); + } + }; + } + + @Test(expected = NullPointerException.class) + public void testLinterRuleNullTitle() { + LinterRule.of(LinterType.INFO, null, (r, a, p, cb) -> {}); + } + + @Test(expected = NullPointerException.class) + public void testLinterIssueNullRule() { + new LinterIssue(null, of('a'), "Description"); + } + + @Test(expected = NullPointerException.class) + public void testLinterIssueNullParser() { + new LinterIssue(new CharacterRepeaterRule(), null, "Description"); + } + + @Test(expected = NullPointerException.class) + public void testLinterIssueNullDescription() { + new LinterIssue(new CharacterRepeaterRule(), of('a'), null); + } + + @Test(expected = NullPointerException.class) + public void testFormatIterableNull() { + LinterRule.formatIterable(null); + } + + @Test(expected = NullPointerException.class) + public void testIsParserIterableEqualNullFirst() { + LinterRule.isParserIterableEqual(null, List.of()); + } + + @Test(expected = NullPointerException.class) + public void testIsParserIterableEqualNullSecond() { + LinterRule.isParserIterableEqual(List.of(), null); + } +} diff --git a/petitparser-core/src/test/java/org/petitparser/utils/tuples/TupleTest.java b/petitparser-core/src/test/java/org/petitparser/utils/tuples/TupleTest.java new file mode 100644 index 0000000..443b897 --- /dev/null +++ b/petitparser-core/src/test/java/org/petitparser/utils/tuples/TupleTest.java @@ -0,0 +1,407 @@ +package org.petitparser.utils.tuples; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +/** + * Tests {@link Tuple} hierarchy (Tuple2 to Tuple9). + */ +public class TupleTest { + + @Test + public void testTuple2() { + Tuple2 t = Tuple.of("a", 1); + assertEquals("a", t.first()); + assertEquals(Integer.valueOf(1), t.second()); + assertEquals(2, t.size()); + assertEquals("a", t.get(0)); + assertEquals(1, t.get(1)); + + List expected = Arrays.asList("a", 1); + assertEquals(expected, t); + assertEquals(t, expected); + assertEquals(expected.hashCode(), t.hashCode()); + assertEquals("[a, 1]", t.toString()); + + assertEquals(t, t); + assertEquals(t, new Tuple2<>("a", 1)); + assertNotEquals(t, new Tuple2<>("b", 1)); + assertNotEquals(t, new Tuple2<>("a", 2)); + assertNotEquals(t, Arrays.asList("a", 2)); + assertNotEquals(t, Arrays.asList("a")); + assertNotEquals(t, Arrays.asList("a", 1, "extra")); + assertNotEquals(t, "not a list"); + assertNotEquals(t, null); + + Tuple2 nullTuple1 = new Tuple2<>(null, null); + Tuple2 nullTuple2 = new Tuple2<>(null, null); + assertEquals(nullTuple1, nullTuple2); + assertEquals(Arrays.asList(null, null), nullTuple1); + assertEquals(Arrays.asList(null, null).hashCode(), nullTuple1.hashCode()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple2GetNegative() { + Tuple.of("a", 1).get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple2GetOutOfBounds() { + Tuple.of("a", 1).get(2); + } + + @Test(expected = UnsupportedOperationException.class) + public void testTuple2ImmutableAdd() { + Tuple.of("a", 1).add("c"); + } + + @Test + public void testTuple3() { + Tuple3 t = Tuple.of("a", 1, 2.5); + assertEquals("a", t.first()); + assertEquals(Integer.valueOf(1), t.second()); + assertEquals(Double.valueOf(2.5), t.third()); + assertEquals(3, t.size()); + assertEquals("a", t.get(0)); + assertEquals(1, t.get(1)); + assertEquals(2.5, t.get(2)); + + List expected = Arrays.asList("a", 1, 2.5); + assertEquals(expected, t); + assertEquals(t, expected); + assertEquals(expected.hashCode(), t.hashCode()); + assertEquals("[a, 1, 2.5]", t.toString()); + + assertEquals(t, t); + assertEquals(t, new Tuple3<>("a", 1, 2.5)); + assertNotEquals(t, new Tuple3<>("b", 1, 2.5)); + assertNotEquals(t, new Tuple3<>("a", 2, 2.5)); + assertNotEquals(t, new Tuple3<>("a", 1, 3.5)); + assertNotEquals(t, Arrays.asList("b", 1, 2.5)); + assertNotEquals(t, Arrays.asList("a", 2, 2.5)); + assertNotEquals(t, Arrays.asList("a", 1, 3.5)); + assertNotEquals(t, Arrays.asList("a", 1)); + assertNotEquals(t, Arrays.asList("a", 1, 2.5, "extra")); + assertNotEquals(t, "str"); + assertNotEquals(t, null); + + Tuple3 nullTuple = new Tuple3<>(null, null, null); + assertEquals(Arrays.asList(null, null, null).hashCode(), nullTuple.hashCode()); + + Tuple3 nullT = new Tuple3<>(null, null, null); + assertEquals(nullT, Arrays.asList(null, null, null)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple3GetOutOfBounds() { + Tuple.of("a", 1, 2.5).get(3); + } + + @Test + public void testTuple4() { + Tuple4 t = Tuple.of("a", 1, 2.5, true); + assertEquals("a", t.first()); + assertEquals(Integer.valueOf(1), t.second()); + assertEquals(Double.valueOf(2.5), t.third()); + assertEquals(Boolean.TRUE, t.fourth()); + assertEquals(4, t.size()); + assertEquals("a", t.get(0)); + assertEquals(1, t.get(1)); + assertEquals(2.5, t.get(2)); + assertEquals(true, t.get(3)); + + List expected = Arrays.asList("a", 1, 2.5, true); + assertEquals(expected, t); + assertEquals(expected.hashCode(), t.hashCode()); + assertEquals("[a, 1, 2.5, true]", t.toString()); + + assertEquals(t, t); + assertEquals(t, new Tuple4<>("a", 1, 2.5, true)); + assertNotEquals(t, new Tuple4<>("x", 1, 2.5, true)); + assertNotEquals(t, new Tuple4<>("a", 2, 2.5, true)); + assertNotEquals(t, new Tuple4<>("a", 1, 3.5, true)); + assertNotEquals(t, new Tuple4<>("a", 1, 2.5, false)); + assertNotEquals(t, Arrays.asList("x", 1, 2.5, true)); + assertNotEquals(t, Arrays.asList("a", 1, 2.5, false)); + assertNotEquals(t, Arrays.asList("a", 1, 2.5)); + assertNotEquals(t, Arrays.asList("a", 1, 2.5, true, "extra")); + assertNotEquals(t, "not a list"); + assertNotEquals(t, null); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple4GetOutOfBounds() { + Tuple.of("a", 1, 2.5, true).get(4); + } + + @Test + public void testTuple5() { + Tuple5 t = Tuple.of(1, 2, 3, 4, 5); + assertEquals(Integer.valueOf(1), t.first()); + assertEquals(Integer.valueOf(2), t.second()); + assertEquals(Integer.valueOf(3), t.third()); + assertEquals(Integer.valueOf(4), t.fourth()); + assertEquals(Integer.valueOf(5), t.fifth()); + assertEquals(5, t.size()); + for (int i = 0; i < 5; i++) { + assertEquals(i + 1, t.get(i)); + } + + List expected = Arrays.asList(1, 2, 3, 4, 5); + assertEquals(expected, t); + assertEquals(expected.hashCode(), t.hashCode()); + assertEquals("[1, 2, 3, 4, 5]", t.toString()); + + assertEquals(t, t); + assertEquals(t, new Tuple5<>(1, 2, 3, 4, 5)); + assertNotEquals(t, new Tuple5<>(0, 2, 3, 4, 5)); + assertNotEquals(t, new Tuple5<>(1, 0, 3, 4, 5)); + assertNotEquals(t, new Tuple5<>(1, 2, 0, 4, 5)); + assertNotEquals(t, new Tuple5<>(1, 2, 3, 0, 5)); + assertNotEquals(t, new Tuple5<>(1, 2, 3, 4, 0)); + assertNotEquals(t, Arrays.asList(0, 2, 3, 4, 5)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 0)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6)); + assertNotEquals(t, "string"); + assertNotEquals(t, null); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple5GetOutOfBounds() { + Tuple.of(1, 2, 3, 4, 5).get(5); + } + + @Test + public void testTuple6() { + Tuple6 t = Tuple.of(1, 2, 3, 4, 5, 6); + assertEquals(Integer.valueOf(1), t.first()); + assertEquals(Integer.valueOf(2), t.second()); + assertEquals(Integer.valueOf(3), t.third()); + assertEquals(Integer.valueOf(4), t.fourth()); + assertEquals(Integer.valueOf(5), t.fifth()); + assertEquals(Integer.valueOf(6), t.sixth()); + assertEquals(6, t.size()); + for (int i = 0; i < 6; i++) { + assertEquals(i + 1, t.get(i)); + } + + List expected = Arrays.asList(1, 2, 3, 4, 5, 6); + assertEquals(expected, t); + assertEquals(expected.hashCode(), t.hashCode()); + assertEquals("[1, 2, 3, 4, 5, 6]", t.toString()); + + assertEquals(t, t); + assertEquals(t, new Tuple6<>(1, 2, 3, 4, 5, 6)); + assertNotEquals(t, new Tuple6<>(0, 2, 3, 4, 5, 6)); + assertNotEquals(t, new Tuple6<>(1, 0, 3, 4, 5, 6)); + assertNotEquals(t, new Tuple6<>(1, 2, 0, 4, 5, 6)); + assertNotEquals(t, new Tuple6<>(1, 2, 3, 0, 5, 6)); + assertNotEquals(t, new Tuple6<>(1, 2, 3, 4, 0, 6)); + assertNotEquals(t, new Tuple6<>(1, 2, 3, 4, 5, 0)); + assertNotEquals(t, Arrays.asList(0, 2, 3, 4, 5, 6)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 0)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6, 7)); + assertNotEquals(t, "not a list"); + assertNotEquals(t, null); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple6GetOutOfBounds() { + Tuple.of(1, 2, 3, 4, 5, 6).get(6); + } + + @Test + public void testTuple7() { + Tuple7 t = + Tuple.of(1, 2, 3, 4, 5, 6, 7); + assertEquals(Integer.valueOf(1), t.first()); + assertEquals(Integer.valueOf(2), t.second()); + assertEquals(Integer.valueOf(3), t.third()); + assertEquals(Integer.valueOf(4), t.fourth()); + assertEquals(Integer.valueOf(5), t.fifth()); + assertEquals(Integer.valueOf(6), t.sixth()); + assertEquals(Integer.valueOf(7), t.seventh()); + assertEquals(7, t.size()); + for (int i = 0; i < 7; i++) { + assertEquals(i + 1, t.get(i)); + } + + List expected = Arrays.asList(1, 2, 3, 4, 5, 6, 7); + assertEquals(expected, t); + assertEquals(expected.hashCode(), t.hashCode()); + assertEquals("[1, 2, 3, 4, 5, 6, 7]", t.toString()); + + assertEquals(t, t); + assertEquals(t, new Tuple7<>(1, 2, 3, 4, 5, 6, 7)); + assertNotEquals(t, new Tuple7<>(0, 2, 3, 4, 5, 6, 7)); + assertNotEquals(t, new Tuple7<>(1, 0, 3, 4, 5, 6, 7)); + assertNotEquals(t, new Tuple7<>(1, 2, 0, 4, 5, 6, 7)); + assertNotEquals(t, new Tuple7<>(1, 2, 3, 0, 5, 6, 7)); + assertNotEquals(t, new Tuple7<>(1, 2, 3, 4, 0, 6, 7)); + assertNotEquals(t, new Tuple7<>(1, 2, 3, 4, 5, 0, 7)); + assertNotEquals(t, new Tuple7<>(1, 2, 3, 4, 5, 6, 0)); + assertNotEquals(t, Arrays.asList(0, 2, 3, 4, 5, 6, 7)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6, 0)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8)); + assertNotEquals(t, 42); + assertNotEquals(t, null); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple7GetOutOfBounds() { + Tuple.of(1, 2, 3, 4, 5, 6, 7).get(7); + } + + @Test + public void testTuple8() { + Tuple8 t = + Tuple.of(1, 2, 3, 4, 5, 6, 7, 8); + assertEquals(Integer.valueOf(1), t.first()); + assertEquals(Integer.valueOf(2), t.second()); + assertEquals(Integer.valueOf(3), t.third()); + assertEquals(Integer.valueOf(4), t.fourth()); + assertEquals(Integer.valueOf(5), t.fifth()); + assertEquals(Integer.valueOf(6), t.sixth()); + assertEquals(Integer.valueOf(7), t.seventh()); + assertEquals(Integer.valueOf(8), t.eighth()); + assertEquals(8, t.size()); + for (int i = 0; i < 8; i++) { + assertEquals(i + 1, t.get(i)); + } + + List expected = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); + assertEquals(expected, t); + assertEquals(expected.hashCode(), t.hashCode()); + assertEquals("[1, 2, 3, 4, 5, 6, 7, 8]", t.toString()); + + assertEquals(t, t); + assertEquals(t, new Tuple8<>(1, 2, 3, 4, 5, 6, 7, 8)); + assertNotEquals(t, new Tuple8<>(0, 2, 3, 4, 5, 6, 7, 8)); + assertNotEquals(t, new Tuple8<>(1, 0, 3, 4, 5, 6, 7, 8)); + assertNotEquals(t, new Tuple8<>(1, 2, 0, 4, 5, 6, 7, 8)); + assertNotEquals(t, new Tuple8<>(1, 2, 3, 0, 5, 6, 7, 8)); + assertNotEquals(t, new Tuple8<>(1, 2, 3, 4, 0, 6, 7, 8)); + assertNotEquals(t, new Tuple8<>(1, 2, 3, 4, 5, 0, 7, 8)); + assertNotEquals(t, new Tuple8<>(1, 2, 3, 4, 5, 6, 0, 8)); + assertNotEquals(t, new Tuple8<>(1, 2, 3, 4, 5, 6, 7, 0)); + assertNotEquals(t, Arrays.asList(0, 2, 3, 4, 5, 6, 7, 8)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 0)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6, 7)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); + assertNotEquals(t, "not a list"); + assertNotEquals(t, null); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple8GetOutOfBounds() { + Tuple.of(1, 2, 3, 4, 5, 6, 7, 8).get(8); + } + + @Test + public void testTuple9() { + Tuple9 t = + Tuple.of(1, 2, 3, 4, 5, 6, 7, 8, 9); + assertEquals(Integer.valueOf(1), t.first()); + assertEquals(Integer.valueOf(2), t.second()); + assertEquals(Integer.valueOf(3), t.third()); + assertEquals(Integer.valueOf(4), t.fourth()); + assertEquals(Integer.valueOf(5), t.fifth()); + assertEquals(Integer.valueOf(6), t.sixth()); + assertEquals(Integer.valueOf(7), t.seventh()); + assertEquals(Integer.valueOf(8), t.eighth()); + assertEquals(Integer.valueOf(9), t.ninth()); + assertEquals(9, t.size()); + for (int i = 0; i < 9; i++) { + assertEquals(i + 1, t.get(i)); + } + + List expected = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); + assertEquals(expected, t); + assertEquals(expected.hashCode(), t.hashCode()); + assertEquals("[1, 2, 3, 4, 5, 6, 7, 8, 9]", t.toString()); + + assertEquals(t, t); + assertEquals(t, new Tuple9<>(1, 2, 3, 4, 5, 6, 7, 8, 9)); + assertNotEquals(t, new Tuple9<>(0, 2, 3, 4, 5, 6, 7, 8, 9)); + assertNotEquals(t, new Tuple9<>(1, 0, 3, 4, 5, 6, 7, 8, 9)); + assertNotEquals(t, new Tuple9<>(1, 2, 0, 4, 5, 6, 7, 8, 9)); + assertNotEquals(t, new Tuple9<>(1, 2, 3, 0, 5, 6, 7, 8, 9)); + assertNotEquals(t, new Tuple9<>(1, 2, 3, 4, 0, 6, 7, 8, 9)); + assertNotEquals(t, new Tuple9<>(1, 2, 3, 4, 5, 0, 7, 8, 9)); + assertNotEquals(t, new Tuple9<>(1, 2, 3, 4, 5, 6, 0, 8, 9)); + assertNotEquals(t, new Tuple9<>(1, 2, 3, 4, 5, 6, 7, 0, 9)); + assertNotEquals(t, new Tuple9<>(1, 2, 3, 4, 5, 6, 7, 8, 0)); + assertNotEquals(t, Arrays.asList(0, 2, 3, 4, 5, 6, 7, 8, 9)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 0)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8)); + assertNotEquals(t, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + assertNotEquals(t, "not a list"); + assertNotEquals(t, false); + assertNotEquals(t, null); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple9GetOutOfBounds() { + Tuple.of(1, 2, 3, 4, 5, 6, 7, 8, 9).get(9); + } + + @Test + public void testTuplesWithNulls() { + assertEquals(new Tuple4<>(null, null, null, null), Arrays.asList(null, null, null, null)); + assertEquals(new Tuple5<>(null, null, null, null, null), Arrays.asList(null, null, null, null, null)); + assertEquals(new Tuple6<>(null, null, null, null, null, null), Arrays.asList(null, null, null, null, null, null)); + assertEquals(new Tuple7<>(null, null, null, null, null, null, null), Arrays.asList(null, null, null, null, null, null, null)); + assertEquals(new Tuple8<>(null, null, null, null, null, null, null, null), Arrays.asList(null, null, null, null, null, null, null, null)); + assertEquals(new Tuple9<>(null, null, null, null, null, null, null, null, null), Arrays.asList(null, null, null, null, null, null, null, null, null)); + + assertEquals(new Tuple4<>(null, null, null, null).hashCode(), Arrays.asList(null, null, null, null).hashCode()); + assertEquals(new Tuple5<>(null, null, null, null, null).hashCode(), Arrays.asList(null, null, null, null, null).hashCode()); + assertEquals(new Tuple6<>(null, null, null, null, null, null).hashCode(), Arrays.asList(null, null, null, null, null, null).hashCode()); + assertEquals(new Tuple7<>(null, null, null, null, null, null, null).hashCode(), Arrays.asList(null, null, null, null, null, null, null).hashCode()); + assertEquals(new Tuple8<>(null, null, null, null, null, null, null, null).hashCode(), Arrays.asList(null, null, null, null, null, null, null, null).hashCode()); + assertEquals(new Tuple9<>(null, null, null, null, null, null, null, null, null).hashCode(), Arrays.asList(null, null, null, null, null, null, null, null, null).hashCode()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple3NegativeIndex() { + Tuple.of(1, 2, 3).get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple4NegativeIndex() { + Tuple.of(1, 2, 3, 4).get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple5NegativeIndex() { + Tuple.of(1, 2, 3, 4, 5).get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple6NegativeIndex() { + Tuple.of(1, 2, 3, 4, 5, 6).get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple7NegativeIndex() { + Tuple.of(1, 2, 3, 4, 5, 6, 7).get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple8NegativeIndex() { + Tuple.of(1, 2, 3, 4, 5, 6, 7, 8).get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testTuple9NegativeIndex() { + Tuple.of(1, 2, 3, 4, 5, 6, 7, 8, 9).get(-1); + } +} diff --git a/petitparser-json/pom.xml b/petitparser-json/pom.xml index 90dcb0d..6c3d264 100644 --- a/petitparser-json/pom.xml +++ b/petitparser-json/pom.xml @@ -7,7 +7,7 @@ com.github.petitparser petitparser - 2.4.2 + 2.5.0 ../pom.xml @@ -19,7 +19,7 @@ com.github.petitparser petitparser-core - 2.4.2 + 2.5.0 diff --git a/petitparser-json/src/main/java/module-info.java b/petitparser-json/src/main/java/module-info.java index f8afcad..9aca4be 100644 --- a/petitparser-json/src/main/java/module-info.java +++ b/petitparser-json/src/main/java/module-info.java @@ -1,4 +1,4 @@ module petitparser.json { - requires petitparser.core; + requires transitive petitparser.core; exports org.petitparser.grammar.json; -} \ No newline at end of file +} diff --git a/petitparser-smalltalk/pom.xml b/petitparser-smalltalk/pom.xml index 4d5cd12..52aa6cb 100644 --- a/petitparser-smalltalk/pom.xml +++ b/petitparser-smalltalk/pom.xml @@ -7,7 +7,7 @@ com.github.petitparser petitparser - 2.4.2 + 2.5.0 ../pom.xml @@ -19,7 +19,7 @@ com.github.petitparser petitparser-core - 2.4.2 + 2.5.0 diff --git a/petitparser-smalltalk/src/main/java/module-info.java b/petitparser-smalltalk/src/main/java/module-info.java index 986bfad..3712a29 100644 --- a/petitparser-smalltalk/src/main/java/module-info.java +++ b/petitparser-smalltalk/src/main/java/module-info.java @@ -1,4 +1,4 @@ module petitparser.smalltalk { - requires petitparser.core; + requires transitive petitparser.core; exports org.petitparser.grammar.smalltalk; -} \ No newline at end of file +} diff --git a/petitparser-xml/pom.xml b/petitparser-xml/pom.xml index 6c60116..a18aa18 100644 --- a/petitparser-xml/pom.xml +++ b/petitparser-xml/pom.xml @@ -7,7 +7,7 @@ com.github.petitparser petitparser - 2.4.2 + 2.5.0 ../pom.xml @@ -19,7 +19,7 @@ com.github.petitparser petitparser-core - 2.4.2 + 2.5.0 diff --git a/petitparser-xml/src/main/java/module-info.java b/petitparser-xml/src/main/java/module-info.java index 0a09dce..bad9fc0 100644 --- a/petitparser-xml/src/main/java/module-info.java +++ b/petitparser-xml/src/main/java/module-info.java @@ -1,4 +1,5 @@ module petitparser.xml { - requires petitparser.core; + requires transitive petitparser.core; exports org.petitparser.grammar.xml; -} \ No newline at end of file + exports org.petitparser.grammar.xml.ast; +} diff --git a/petitparser-xml/src/main/java/org/petitparser/grammar/xml/XmlBuilder.java b/petitparser-xml/src/main/java/org/petitparser/grammar/xml/XmlBuilder.java index 231e1e0..a3b0aea 100644 --- a/petitparser-xml/src/main/java/org/petitparser/grammar/xml/XmlBuilder.java +++ b/petitparser-xml/src/main/java/org/petitparser/grammar/xml/XmlBuilder.java @@ -18,6 +18,8 @@ */ public class XmlBuilder implements XmlCallback { + public XmlBuilder() {} + @Override public XmlNode createAttribute(XmlName name, String text) { return new XmlAttribute(name, text); diff --git a/petitparser-xml/src/main/java/org/petitparser/grammar/xml/XmlCharacterParser.java b/petitparser-xml/src/main/java/org/petitparser/grammar/xml/XmlCharacterParser.java index 1633d6c..c6d4183 100644 --- a/petitparser-xml/src/main/java/org/petitparser/grammar/xml/XmlCharacterParser.java +++ b/petitparser-xml/src/main/java/org/petitparser/grammar/xml/XmlCharacterParser.java @@ -21,9 +21,8 @@ public class XmlCharacterParser extends Parser { // character mapping - @SuppressWarnings("DoubleBraceInitialization") private static final Map NAME_TO_CHAR = - new HashMap() {{ + new HashMap<>() {{ // xml entities put("lt", '<'); diff --git a/petitparser-xml/src/main/java/org/petitparser/grammar/xml/ast/XmlNode.java b/petitparser-xml/src/main/java/org/petitparser/grammar/xml/ast/XmlNode.java index faa9311..e367f3d 100644 --- a/petitparser-xml/src/main/java/org/petitparser/grammar/xml/ast/XmlNode.java +++ b/petitparser-xml/src/main/java/org/petitparser/grammar/xml/ast/XmlNode.java @@ -10,6 +10,8 @@ */ public abstract class XmlNode implements Iterable { + protected XmlNode() {} + private XmlNode parent; /** diff --git a/pom.xml b/pom.xml index ab3fd88..98ef3d6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 - 11 + 21 UTF-8 UTF-8 @@ -13,7 +13,7 @@ com.github.petitparser petitparser - 2.4.2 + 2.5.0 pom PetitParser @@ -56,8 +56,8 @@ - Travis - https://travis-ci.org/petitparser/java-petitparser + GitHub Actions + https://github.com/petitparser/java-petitparser/actions @@ -91,7 +91,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.16.0 @@ -100,13 +100,13 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.16.0 org.apache.maven.plugins maven-source-plugin - 3.3.1 + 3.4.0 attach-sources @@ -120,7 +120,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.8.0 + 3.12.0 -Xdoclint:none -Xdoclint:none @@ -135,6 +135,34 @@ + + + com.diffplug.spotless + spotless-maven-plugin + 3.10.4 + + + + src/main/java/**/*.java + src/test/java/**/*.java + + + + + true + 2 + + + + + + + check + + verify + + + @@ -159,7 +187,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.7 + 3.2.8 sign-artifacts