[jsemver] 84/95: Refactor VersionParser to improve error handling

Alexandre Viau reazem-guest at moszumanska.debian.org
Mon Feb 16 14:58:33 UTC 2015


This is an automated email from the git hooks/post-receive script.

reazem-guest pushed a commit to branch master
in repository jsemver.

commit d36d96155500af4d42dcf2fdbbbfdcd01cf9180e
Author: Zafar Khaja <zafarkhaja at gmail.com>
Date:   Mon Jun 23 21:32:16 2014 +0300

    Refactor VersionParser to improve error handling
---
 .../java/com/github/zafarkhaja/semver/Version.java |  14 +++
 .../github/zafarkhaja/semver/VersionParser.java    |  79 +++++++++++----
 .../zafarkhaja/semver/ParserErrorHandlingTest.java | 108 +++++++++++++++++++++
 3 files changed, 182 insertions(+), 19 deletions(-)

diff --git a/src/main/java/com/github/zafarkhaja/semver/Version.java b/src/main/java/com/github/zafarkhaja/semver/Version.java
index 9d9a3a2..3e33603 100644
--- a/src/main/java/com/github/zafarkhaja/semver/Version.java
+++ b/src/main/java/com/github/zafarkhaja/semver/Version.java
@@ -139,6 +139,8 @@ public class Version implements Comparable<Version> {
          * Builds a {@code Version} object.
          *
          * @return a newly built {@code Version} instance
+         * @throws ParseException when invalid version string is provided
+         * @throws UnexpectedCharacterException is a special case of {@code ParseException}
          */
         public Version build() {
             StringBuilder sb = new StringBuilder();
@@ -254,6 +256,8 @@ public class Version implements Comparable<Version> {
      * @param version the version string to parse
      * @return a new instance of the {@code Version} class
      * @throws IllegalArgumentException if the input string is {@code NULL} or empty
+     * @throws ParseException when invalid version string is provided
+     * @throws UnexpectedCharacterException is a special case of {@code ParseException}
      */
     public static Version valueOf(String version) {
         return VersionParser.parseValidSemVer(version);
@@ -328,6 +332,8 @@ public class Version implements Comparable<Version> {
      * @param preRelease the pre-release version to append
      * @return a new instance of the {@code Version} class
      * @throws IllegalArgumentException if the input string is {@code NULL} or empty
+     * @throws ParseException when invalid version string is provided
+     * @throws UnexpectedCharacterException is a special case of {@code ParseException}
      */
     public Version incrementMajorVersion(String preRelease) {
         return new Version(
@@ -351,6 +357,8 @@ public class Version implements Comparable<Version> {
      * @param preRelease the pre-release version to append
      * @return a new instance of the {@code Version} class
      * @throws IllegalArgumentException if the input string is {@code NULL} or empty
+     * @throws ParseException when invalid version string is provided
+     * @throws UnexpectedCharacterException is a special case of {@code ParseException}
      */
     public Version incrementMinorVersion(String preRelease) {
         return new Version(
@@ -374,6 +382,8 @@ public class Version implements Comparable<Version> {
      * @param preRelease the pre-release version to append
      * @return a new instance of the {@code Version} class
      * @throws IllegalArgumentException if the input string is {@code NULL} or empty
+     * @throws ParseException when invalid version string is provided
+     * @throws UnexpectedCharacterException is a special case of {@code ParseException}
      */
     public Version incrementPatchVersion(String preRelease) {
         return new Version(
@@ -406,6 +416,8 @@ public class Version implements Comparable<Version> {
      * @param preRelease the pre-release version to set
      * @return a new instance of the {@code Version} class
      * @throws IllegalArgumentException if the input string is {@code NULL} or empty
+     * @throws ParseException when invalid version string is provided
+     * @throws UnexpectedCharacterException is a special case of {@code ParseException}
      */
     public Version setPreReleaseVersion(String preRelease) {
         return new Version(normal, VersionParser.parsePreRelease(preRelease));
@@ -417,6 +429,8 @@ public class Version implements Comparable<Version> {
      * @param build the build metadata to set
      * @return a new instance of the {@code Version} class
      * @throws IllegalArgumentException if the input string is {@code NULL} or empty
+     * @throws ParseException when invalid version string is provided
+     * @throws UnexpectedCharacterException is a special case of {@code ParseException}
      */
     public Version setBuildMetadata(String build) {
         return new Version(normal, preRelease, VersionParser.parseBuild(build));
diff --git a/src/main/java/com/github/zafarkhaja/semver/VersionParser.java b/src/main/java/com/github/zafarkhaja/semver/VersionParser.java
index 5347a19..31dca2c 100644
--- a/src/main/java/com/github/zafarkhaja/semver/VersionParser.java
+++ b/src/main/java/com/github/zafarkhaja/semver/VersionParser.java
@@ -257,15 +257,19 @@ class VersionParser implements Parser<Version> {
     private Version parseValidSemVer() {
         NormalVersion normal = parseVersionCore();
         MetadataVersion preRelease = MetadataVersion.NULL;
-        if (chars.positiveLookahead(HYPHEN)) {
-            chars.consume();
-            preRelease = parsePreRelease();
-        }
         MetadataVersion build = MetadataVersion.NULL;
-        if (chars.positiveLookahead(PLUS)) {
-            chars.consume();
+
+        Character next = consumeNextCharacter(HYPHEN, PLUS, EOL);
+        if (HYPHEN.isMatchedBy(next)) {
+            preRelease = parsePreRelease();
+            next = consumeNextCharacter(PLUS, EOL);
+            if (PLUS.isMatchedBy(next)) {
+                build = parseBuild();
+            }
+        } else if (PLUS.isMatchedBy(next)) {
             build = parseBuild();
         }
+        consumeNextCharacter(EOL);
         return new Version(normal, preRelease, build);
     }
 
@@ -282,9 +286,9 @@ class VersionParser implements Parser<Version> {
      */
     private NormalVersion parseVersionCore() {
         int major = Integer.parseInt(numericIdentifier());
-        chars.consume(DOT);
+        consumeNextCharacter(DOT);
         int minor = Integer.parseInt(numericIdentifier());
-        chars.consume(DOT);
+        consumeNextCharacter(DOT);
         int patch = Integer.parseInt(numericIdentifier());
         return new NormalVersion(major, minor, patch);
     }
@@ -305,14 +309,16 @@ class VersionParser implements Parser<Version> {
      * @throws ParseException if the pre-release version has empty identifier(s)
      */
     private MetadataVersion parsePreRelease() {
+        ensureValidLookahead(DIGIT, LETTER, HYPHEN);
         List<String> idents = new ArrayList<String>();
         do {
-            checkForEmptyIdentifier();
             idents.add(preReleaseIdentifier());
             if (chars.positiveLookahead(DOT)) {
-                chars.consume(DOT);
+                consumeNextCharacter(DOT);
+                continue;
             }
-        } while (!chars.positiveLookahead(PLUS, EOL));
+            break;
+        } while (true);
         return new MetadataVersion(idents.toArray(new String[idents.size()]));
     }
 
@@ -329,6 +335,7 @@ class VersionParser implements Parser<Version> {
      * @return a single pre-release identifier
      */
     private String preReleaseIdentifier() {
+        checkForEmptyIdentifier();
         CharType boundary = nearestCharType(DOT, PLUS, EOL);
         if (chars.positiveLookaheadBefore(boundary, LETTER, HYPHEN)) {
             return alphanumericIdentifier();
@@ -353,14 +360,16 @@ class VersionParser implements Parser<Version> {
      * @throws ParseException if the build metadata has empty identifier(s)
      */
     private MetadataVersion parseBuild() {
+        ensureValidLookahead(DIGIT, LETTER, HYPHEN);
         List<String> idents = new ArrayList<String>();
         do {
-            checkForEmptyIdentifier();
             idents.add(buildIdentifier());
             if (chars.positiveLookahead(DOT)) {
-                chars.consume(DOT);
+                consumeNextCharacter(DOT);
+                continue;
             }
-        } while (!chars.positiveLookahead(EOL));
+            break;
+        } while (true);
         return new MetadataVersion(idents.toArray(new String[idents.size()]));
     }
 
@@ -377,6 +386,7 @@ class VersionParser implements Parser<Version> {
      * @return a single build identifier
      */
     private String buildIdentifier() {
+        checkForEmptyIdentifier();
         CharType boundary = nearestCharType(DOT, EOL);
         if (chars.positiveLookaheadBefore(boundary, LETTER, HYPHEN)) {
             return alphanumericIdentifier();
@@ -421,7 +431,7 @@ class VersionParser implements Parser<Version> {
     private String alphanumericIdentifier() {
         StringBuilder sb = new StringBuilder();
         do {
-            sb.append(chars.consume(DIGIT, LETTER, HYPHEN));
+            sb.append(consumeNextCharacter(DIGIT, LETTER, HYPHEN));
         } while (chars.positiveLookahead(DIGIT, LETTER, HYPHEN));
         return sb.toString();
     }
@@ -441,7 +451,7 @@ class VersionParser implements Parser<Version> {
     private String digits() {
         StringBuilder sb = new StringBuilder();
         do {
-            sb.append(chars.consume(DIGIT));
+            sb.append(consumeNextCharacter(DIGIT));
         } while (chars.positiveLookahead(DIGIT));
         return sb.toString();
     }
@@ -471,7 +481,7 @@ class VersionParser implements Parser<Version> {
     private void checkForLeadingZeroes() {
         Character la1 = chars.lookahead(1);
         Character la2 = chars.lookahead(2);
-        if (la1 == '0' && DIGIT.isMatchedBy(la2)) {
+        if (la1 != null && la1 == '0' && DIGIT.isMatchedBy(la2)) {
             throw new ParseException(
                 "Numeric identifier MUST NOT contain leading zeroes"
             );
@@ -485,8 +495,39 @@ class VersionParser implements Parser<Version> {
      *                        metadata have empty identifier(s)
      */
     private void checkForEmptyIdentifier() {
-        if (DOT.isMatchedBy(chars.lookahead(1))) {
-            throw new ParseException("Identifiers MUST NOT be empty");
+        Character la = chars.lookahead(1);
+        if (DOT.isMatchedBy(la) || PLUS.isMatchedBy(la) || EOL.isMatchedBy(la)) {
+            throw new ParseException(
+                "Identifiers MUST NOT be empty",
+                new UnexpectedCharacterException(la, DIGIT, LETTER, HYPHEN)
+            );
+        }
+    }
+
+    /**
+     * Tries to consume the next character in the stream.
+     *
+     * @param expected the expected types of the next character
+     * @return the next character in the stream
+     * @throws UnexpectedCharacterException if the next element is of an unexpected type
+     */
+    private Character consumeNextCharacter(CharType... expected) {
+        try {
+            return chars.consume(expected);
+        } catch (UnexpectedElementException e) {
+            throw new UnexpectedCharacterException(e);
+        }
+    }
+
+    /**
+     * Checks if the next character in the stream is valid.
+     *
+     * @param expected the expected types of the next character
+     * @throws UnexpectedCharacterException if the next element is not valid
+     */
+    private void ensureValidLookahead(CharType... expected) {
+        if (!chars.positiveLookahead(expected)) {
+            throw new UnexpectedCharacterException(chars.lookahead(1), expected);
         }
     }
 }
diff --git a/src/test/java/com/github/zafarkhaja/semver/ParserErrorHandlingTest.java b/src/test/java/com/github/zafarkhaja/semver/ParserErrorHandlingTest.java
new file mode 100644
index 0000000..d3e0738
--- /dev/null
+++ b/src/test/java/com/github/zafarkhaja/semver/ParserErrorHandlingTest.java
@@ -0,0 +1,108 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2014 Zafar Khaja <zafarkhaja at gmail.com>.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.github.zafarkhaja.semver;
+
+import com.github.zafarkhaja.semver.VersionParser.CharType;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import static com.github.zafarkhaja.semver.VersionParser.CharType.*;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author Zafar Khaja <zafarkhaja at gmail.com>
+ */
+ at RunWith(Parameterized.class)
+public class ParserErrorHandlingTest {
+
+    private final String invalidVersion;
+    private final Character unexpected;
+    private final CharType[] expected;
+
+    public ParserErrorHandlingTest(
+        String invalidVersion,
+        Character unexpected,
+        CharType[] expected
+    ) {
+        this.invalidVersion = invalidVersion;
+        this.unexpected = unexpected;
+        this.expected = expected;
+    }
+
+    @Test
+    public void shouldCorrectlyHandleParseErrors() {
+        try {
+            VersionParser.parseValidSemVer(invalidVersion);
+        } catch (UnexpectedCharacterException e) {
+            assertEquals(unexpected, e.getUnexpectedCharacter());
+            assertArrayEquals(expected, e.getExpectedCharTypes());
+            return;
+        } catch (ParseException e) {
+            if (e.getCause() != null) {
+                UnexpectedCharacterException cause = (UnexpectedCharacterException) e.getCause();
+                assertEquals(unexpected, cause.getUnexpectedCharacter());
+                assertArrayEquals(expected, cause.getExpectedCharTypes());
+            }
+            return;
+        }
+        fail("Uncaught exception");
+    }
+
+    @Parameters(name = "{0}")
+    public static Collection<Object[]> parameters() {
+        return Arrays.asList(new Object[][] {
+            { "1",                null, new CharType[] { DOT } },
+            { "1 ",               ' ',  new CharType[] { DOT } },
+            { "1.",               null, new CharType[] { DIGIT } },
+            { "1.2",              null, new CharType[] { DOT } },
+            { "1.2.",             null, new CharType[] { DIGIT } },
+            { "a.b.c",            'a',  new CharType[] { DIGIT } },
+            { "1.b.c",            'b',  new CharType[] { DIGIT } },
+            { "1.2.c",            'c',  new CharType[] { DIGIT } },
+            { "!.2.3",            '!',  new CharType[] { DIGIT } },
+            { "1.!.3",            '!',  new CharType[] { DIGIT } },
+            { "1.2.!",            '!',  new CharType[] { DIGIT } },
+            { "v1.2.3",           'v',  new CharType[] { DIGIT } },
+            { "1.2.3-",           null, new CharType[] { DIGIT, LETTER, HYPHEN } },
+            { "1.2. 3",           ' ',  new CharType[] { DIGIT } },
+            { "1.2.3=alpha",      '=',  new CharType[] { HYPHEN, PLUS, EOL } },
+            { "1.2.3~beta",       '~',  new CharType[] { HYPHEN, PLUS, EOL } },
+            { "1.2.3-be$ta",      '$',  new CharType[] { PLUS, EOL } },
+            { "1.2.3+b1+b2",      '+',  new CharType[] { EOL } },
+            { "1.2.3-rc!",        '!',  new CharType[] { PLUS, EOL } },
+            { "1.2.3-+",          '+',  new CharType[] { DIGIT, LETTER, HYPHEN } },
+            { "1.2.3-@",          '@',  new CharType[] { DIGIT, LETTER, HYPHEN } },
+            { "1.2.3+@",          '@',  new CharType[] { DIGIT, LETTER, HYPHEN } },
+            { "1.2.3-rc1.",       null, new CharType[] { DIGIT, LETTER, HYPHEN } },
+            { "1.2.3+20140620.",  null, new CharType[] { DIGIT, LETTER, HYPHEN } },
+            { "1.2.3-b.+b",       '+',  new CharType[] { DIGIT, LETTER, HYPHEN } },
+            { "1.2.3-rc..",       '.',  new CharType[] { DIGIT, LETTER, HYPHEN } },
+            { "1.2.3-rc+bld..",   '.',  new CharType[] { DIGIT, LETTER, HYPHEN } },
+        });
+    }
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/jsemver.git



More information about the pkg-java-commits mailing list