, N e
public abstract Token getLastAntlrToken();
-
@Override
- public FileLocation getReportLocation() {
- return getTextDocument().toLocation(TextRegion.fromBothOffsets(getFirstAntlrToken().getStartIndex(),
- getFirstAntlrToken().getStopIndex()));
+ public TextRegion getTextRegion() {
+ return TextRegion.fromBothOffsets(getFirstAntlrToken().getStartIndex(),
+ getFirstAntlrToken().getStopIndex());
}
void setIndexInParent(int indexInParent) {
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/javacc/AbstractJjtreeNode.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/javacc/AbstractJjtreeNode.java
index ee3e02fea4..5c7073e4bb 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/javacc/AbstractJjtreeNode.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/javacc/AbstractJjtreeNode.java
@@ -59,11 +59,6 @@ public abstract class AbstractJjtreeNode, N e
getLastToken().getEndOffset());
}
- @Override
- public FileLocation getReportLocation() {
- return getTextDocument().toLocation(getTextRegion());
- }
-
@Override
public final int compareLocation(Node other) {
if (other instanceof JjtreeNode>) {
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/Chars.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/Chars.java
index d55562b822..bf82738e74 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/Chars.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/Chars.java
@@ -5,6 +5,7 @@
package net.sourceforge.pmd.lang.document;
+import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
@@ -13,7 +14,10 @@ import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
@@ -34,6 +38,15 @@ import org.checkerframework.checker.nullness.qual.NonNull;
public final class Chars implements CharSequence {
public static final Chars EMPTY = wrap("");
+ /**
+ * Special sentinel used by {@link #lines()}.
+ */
+ private static final int NOT_TRIED = -2;
+
+ /**
+ * See {@link StringUtils#INDEX_NOT_FOUND}.
+ */
+ private static final int NOT_FOUND = -1;
private final String str;
private final int start;
@@ -90,8 +103,8 @@ public final class Chars implements CharSequence {
}
/**
- * Copies 'len' characters from index 'from' into the given array,
- * starting at 'off'.
+ * Copies 'count' characters from index 'srcBegin' into the given array,
+ * starting at 'dstBegin'.
*
* @param srcBegin Start offset in this CharSequence
* @param cbuf Character array
@@ -108,26 +121,21 @@ public final class Chars implements CharSequence {
}
/**
- * Appends the character range identified by offset and length into
+ * Appends the character range identified by start and end offset into
* the string builder. This is much more efficient than calling
* {@link StringBuilder#append(CharSequence)} with this as the
* parameter, especially on Java 9+.
*
- * Be aware that {@link StringBuilder#append(CharSequence, int, int)}
- * takes a start and end offset, whereas this method (like all
- * the others in this class) take a start offset and a length.
- *
- * @param off Start (inclusive)
- * @param len Number of characters
+ * @param start Start index (inclusive)
+ * @param end End index (exclusive)
*
* @throws IndexOutOfBoundsException See {@link StringBuilder#append(CharSequence, int, int)}
*/
- public void appendChars(StringBuilder sb, int off, int len) {
- if (len == 0) {
+ public void appendChars(StringBuilder sb, int start, int end) {
+ if (end == 0) {
return;
}
- int idx = idx(off);
- sb.append(str, idx, idx + len);
+ sb.append(str, idx(start), idx(end));
}
/**
@@ -158,20 +166,20 @@ public final class Chars implements CharSequence {
final int max = start + len - searched.length();
if (fromIndex < 0 || max < start + fromIndex) {
- return -1;
+ return NOT_FOUND;
} else if (searched.isEmpty()) {
return 0;
}
final char fst = searched.charAt(0);
- int strpos = str.indexOf(fst, start + fromIndex);
- while (strpos != -1 && strpos <= max) {
+ int strpos = str.indexOf(fst, idx(fromIndex));
+ while (strpos != NOT_FOUND && strpos <= max) {
if (str.startsWith(searched, strpos)) {
return strpos - start;
}
strpos = str.indexOf(fst, strpos + 1);
}
- return -1;
+ return NOT_FOUND;
}
/**
@@ -179,7 +187,7 @@ public final class Chars implements CharSequence {
*/
public int indexOf(int ch, int fromIndex) {
if (fromIndex < 0 || fromIndex >= len) {
- return -1;
+ return NOT_FOUND;
}
// we want to avoid searching too far in the string
// so we don't use String#indexOf, as it would be looking
@@ -193,7 +201,28 @@ public final class Chars implements CharSequence {
return i - start;
}
}
- return -1;
+ return NOT_FOUND;
+ }
+
+ /**
+ * See {@link String#lastIndexOf(int, int)}.
+ */
+ public int lastIndexOf(int ch, int fromIndex) {
+ if (fromIndex < 0 || fromIndex >= len) {
+ return NOT_FOUND;
+ }
+ // we want to avoid searching too far in the string
+ // so we don't use String#indexOf, as it would be looking
+ // in the rest of the file too, which in the worst case is
+ // horrible
+
+ for (int i = start + fromIndex; i >= start; i--) {
+ char c = str.charAt(i);
+ if (c == ch) {
+ return i - start;
+ }
+ }
+ return NOT_FOUND;
}
/**
@@ -218,7 +247,14 @@ public final class Chars implements CharSequence {
if (fromIndex < 0 || fromIndex + 1 > len) {
return false;
}
- return str.charAt(start + fromIndex) == prefix;
+ return str.charAt(idx(fromIndex)) == prefix;
+ }
+
+ /**
+ * See {@link String#endsWith(String)}.
+ */
+ public boolean endsWith(String suffix) {
+ return startsWith(suffix, length() - suffix.length());
}
/**
@@ -254,6 +290,64 @@ public final class Chars implements CharSequence {
return trimStart().trimEnd();
}
+ /**
+ * Remove trailing and leading blank lines. The resulting string
+ * does not end with a line terminator.
+ */
+ public Chars trimBlankLines() {
+ int offsetOfFirstNonBlankChar = length();
+ for (int i = 0; i < length(); i++) {
+ if (!Character.isWhitespace(charAt(i))) {
+ offsetOfFirstNonBlankChar = i;
+ break;
+ }
+ }
+ int offsetOfLastNonBlankChar = 0;
+ for (int i = length() - 1; i > offsetOfFirstNonBlankChar; i--) {
+ if (!Character.isWhitespace(charAt(i))) {
+ offsetOfLastNonBlankChar = i;
+ break;
+ }
+ }
+
+ // look backwards before the first non-blank char
+ int cutFromInclusive = lastIndexOf('\n', offsetOfFirstNonBlankChar);
+ // If firstNonBlankLineStart == -1, ie we're on the first line,
+ // we want to start at zero: then we add 1 to get 0
+ // If firstNonBlankLineStart >= 0, then it's the index of the
+ // \n, we want to cut right after that, so we add 1.
+ cutFromInclusive += 1;
+
+ // look forwards after the last non-blank char
+ int cutUntilExclusive = indexOf('\n', offsetOfLastNonBlankChar);
+ if (cutUntilExclusive == StringUtils.INDEX_NOT_FOUND) {
+ cutUntilExclusive = length();
+ }
+
+ return subSequence(cutFromInclusive, cutUntilExclusive);
+ }
+
+ /**
+ * Remove the suffix if it is present, otherwise returns this.
+ */
+ public Chars removeSuffix(String charSeq) {
+ int trimmedLen = length() - charSeq.length();
+ if (startsWith(charSeq, trimmedLen)) {
+ return slice(0, trimmedLen);
+ }
+ return this;
+ }
+
+ /**
+ * Remove the prefix if it is present, otherwise returns this.
+ */
+ public Chars removePrefix(String charSeq) {
+ if (startsWith(charSeq)) {
+ return subSequence(charSeq.length(), length());
+ }
+ return this;
+ }
+
/**
* Returns true if this char sequence is logically equal to the
@@ -311,6 +405,14 @@ public final class Chars implements CharSequence {
return slice(start, end - start);
}
+ /**
+ * Returns the subsequence that starts at the given offset and ends
+ * at the end of this string. Similar to {@link String#substring(int)}.
+ */
+ public Chars subSequence(int start) {
+ return slice(start, len - start);
+ }
+
/**
* Slice a region of text.
*
@@ -346,29 +448,32 @@ public final class Chars implements CharSequence {
}
/**
- * Returns the substring starting at the given offset and with the
- * given length. This differs from {@link String#substring(int, int)}
- * in that it uses offset + length instead of start + end.
+ * Returns the substring between the given offsets.
+ * given length.
*
- * @param off Start offset ({@code 0 <= off < this.length()})
- * @param len Length of the substring ({@code 0 <= len <= this.length() - off})
+ *
Note: Unlike slice or subSequence, this method will create a
+ * new String which involves copying the backing char array. Don't
+ * use it unnecessarily.
+ *
+ * @param start Start offset ({@code 0 <= start < this.length()})
+ * @param end End offset ({@code start <= end <= this.length()})
*
* @return A substring
*
* @throws IndexOutOfBoundsException If the parameters are not a valid range
+ * @see String#substring(int, int)
*/
- public String substring(int off, int len) {
- validateRange(off, len, this.len);
- int start = idx(off);
- return str.substring(start, start + len);
+ public String substring(int start, int end) {
+ validateRange(start, end - start, this.len);
+ return str.substring(idx(start), idx(end));
}
private static void validateRangeWithAssert(int off, int len, int bound) {
- assert len >= 0 && off >= 0 && (off + len) <= bound : invalidRange(off, len, bound);
+ assert len >= 0 && off >= 0 && off + len <= bound : invalidRange(off, len, bound);
}
private static void validateRange(int off, int len, int bound) {
- if (len < 0 || off < 0 || (off + len) > bound) {
+ if (len < 0 || off < 0 || off + len > bound) {
throw new IndexOutOfBoundsException(invalidRange(off, len, bound));
}
}
@@ -378,7 +483,7 @@ public final class Chars implements CharSequence {
}
@Override
- public String toString() {
+ public @NonNull String toString() {
// this already avoids the copy if start == 0 && len == str.length()
return str.substring(start, start + len);
}
@@ -406,19 +511,26 @@ public final class Chars implements CharSequence {
return h;
}
- private boolean isFullString() {
+ // test only
+ boolean isFullString() {
return start == 0 && len == str.length();
}
/**
* Returns an iterable over the lines of this char sequence. The lines
- * are yielded without line separators. For the purposes of this method,
- * a line delimiter is {@code LF} or {@code CR+LF}.
+ * are yielded without line separators. Like {@link BufferedReader#readLine()},
+ * a line delimiter is {@code CR}, {@code LF} or {@code CR+LF}.
*/
public Iterable lines() {
return () -> new Iterator() {
final int max = len;
int pos = 0;
+ // If those are NOT_TRIED, then we should scan ahead to find them
+ // If the scan fails then they'll stay -1 forever and won't be tried again.
+ // This is important to scan in documents where we know there are no
+ // CR characters, as in our normalized TextFileContent.
+ int nextCr = NOT_TRIED;
+ int nextLf = NOT_TRIED;
@Override
public boolean hasNext() {
@@ -427,62 +539,138 @@ public final class Chars implements CharSequence {
@Override
public Chars next() {
- int nl = indexOf('\n', pos);
- Chars next;
- if (nl < 0) {
- next = subSequence(pos, max);
- pos = max;
- return next;
- } else if (startsWith('\r', nl - 1)) {
- next = subSequence(pos, nl - 1);
- } else {
- next = subSequence(pos, nl);
+ final int curPos = pos;
+ if (nextCr == NOT_TRIED) {
+ nextCr = indexOf('\r', curPos);
+ }
+ if (nextLf == NOT_TRIED) {
+ nextLf = indexOf('\n', curPos);
+ }
+ final int cr = nextCr;
+ final int lf = nextLf;
+
+ if (cr != NOT_FOUND && lf != NOT_FOUND) {
+ // found both CR and LF
+ int min = Math.min(cr, lf);
+ if (lf == cr + 1) {
+ // CRLF
+ pos = lf + 1;
+ nextCr = NOT_TRIED;
+ nextLf = NOT_TRIED;
+ } else {
+ pos = min + 1;
+ resetLookahead(cr, min);
+ }
+
+ return subSequence(curPos, min);
+ } else if (cr == NOT_FOUND && lf == NOT_FOUND) {
+ // no following line terminator, cut until the end
+ pos = max;
+ return subSequence(curPos, max);
+ } else {
+ // lf or cr (exactly one is != -1 and max returns that one)
+ int idx = Math.max(cr, lf);
+ resetLookahead(cr, idx);
+ pos = idx + 1;
+ return subSequence(curPos, idx);
+ }
+ }
+
+ private void resetLookahead(int cr, int idx) {
+ if (idx == cr) {
+ nextCr = NOT_TRIED;
+ } else {
+ nextLf = NOT_TRIED;
}
- pos = nl + 1;
- return next;
}
};
}
+ /**
+ * Returns a stream of lines yielded by {@link #lines()}.
+ */
+ public Stream lineStream() {
+ return StreamSupport.stream(lines().spliterator(), false);
+ }
+
/**
* Returns a new reader for the whole contents of this char sequence.
*/
public Reader newReader() {
- return new Reader() {
- private int pos = start;
- private final int max = start + len;
+ return new CharsReader(this);
+ }
- @Override
- public int read(char[] cbuf, int off, int len) {
- if (len < 0 || off < 0 || off + len > cbuf.length) {
- throw new IndexOutOfBoundsException();
- }
- if (pos >= max) {
- return -1;
- }
- int toRead = Integer.min(max - pos, len);
- str.getChars(pos, pos + toRead, cbuf, off);
- pos += toRead;
- return toRead;
- }
+ private static final class CharsReader extends Reader {
- @Override
- public int read() {
- return pos >= max ? -1 : str.charAt(pos++);
- }
+ private Chars chars;
+ private int pos;
+ private final int max;
+ private int mark = -1;
- @Override
- public long skip(long n) {
- int oldPos = pos;
- pos = Math.min(max, pos + (int) n);
- return pos - oldPos;
- }
+ private CharsReader(Chars chars) {
+ this.chars = chars;
+ this.pos = chars.start;
+ this.max = chars.start + chars.len;
+ }
- @Override
- public void close() {
- // nothing to do
+ @Override
+ public int read(char @NonNull [] cbuf, int off, int len) throws IOException {
+ if (len < 0 || off < 0 || off + len > cbuf.length) {
+ throw new IndexOutOfBoundsException();
}
- };
+ ensureOpen();
+ if (pos >= max) {
+ return NOT_FOUND;
+ }
+ int toRead = Integer.min(max - pos, len);
+ chars.str.getChars(pos, pos + toRead, cbuf, off);
+ pos += toRead;
+ return toRead;
+ }
+
+ @Override
+ public int read() throws IOException {
+ ensureOpen();
+ return pos >= max ? NOT_FOUND : chars.str.charAt(pos++);
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ ensureOpen();
+ int oldPos = pos;
+ pos = Math.min(max, pos + (int) n);
+ return pos - oldPos;
+ }
+
+ private void ensureOpen() throws IOException {
+ if (chars == null) {
+ throw new IOException("Closed");
+ }
+ }
+
+ @Override
+ public void close() {
+ chars = null;
+ }
+
+ @Override
+ public void mark(int readAheadLimit) {
+ mark = pos;
+ }
+
+ @Override
+ public void reset() throws IOException {
+ ensureOpen();
+ if (mark == -1) {
+ throw new IOException("Reader was not marked");
+ }
+ pos = mark;
+ }
+
+ @Override
+ public boolean markSupported() {
+ return true;
+ }
}
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java
index ef8867dee5..cfe574c851 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java
@@ -48,6 +48,7 @@ import net.sourceforge.pmd.util.log.MessageReporter;
*/
@SuppressWarnings("PMD.CloseResource")
public final class FileCollector implements AutoCloseable {
+
private static final Logger LOG = LoggerFactory.getLogger(FileCollector.class);
private final List allFilesToProcess = new ArrayList<>();
@@ -138,7 +139,7 @@ public final class FileCollector implements AutoCloseable {
*/
public boolean addFile(Path file) {
if (!Files.isRegularFile(file)) {
- reporter.error("Not a regular file {}", file);
+ reporter.error("Not a regular file: {0}", file);
return false;
}
LanguageVersion languageVersion = discoverLanguage(file.toString());
@@ -164,11 +165,14 @@ public final class FileCollector implements AutoCloseable {
public boolean addFile(Path file, Language language) {
AssertionUtil.requireParamNotNull("language", language);
if (!Files.isRegularFile(file)) {
- reporter.error("Not a regular file {}", file);
+ reporter.error("Not a regular file: {0}", file);
return false;
}
- NioTextFile nioTextFile = new NioTextFile(file, charset, discoverer.getDefaultLanguageVersion(language), getDisplayName(file));
- addFileImpl(nioTextFile);
+ LanguageVersion lv = discoverer.getDefaultLanguageVersion(language);
+ Objects.requireNonNull(lv);
+ addFileImpl(TextFile.builderForPath(file, charset, lv)
+ .withDisplayName(getDisplayName(file))
+ .build());
return true;
}
@@ -240,7 +244,7 @@ public final class FileCollector implements AutoCloseable {
LanguageVersion contextVersion = discoverer.getDefaultLanguageVersion(language);
if (!fileVersion.equals(contextVersion)) {
reporter.error(
- "Cannot add file {}: version ''{}'' does not match ''{}''",
+ "Cannot add file {0}: version ''{1}'' does not match ''{2}''",
textFile.getPathId(),
fileVersion,
contextVersion
@@ -283,7 +287,7 @@ public final class FileCollector implements AutoCloseable {
*/
public boolean addDirectory(Path dir) throws IOException {
if (!Files.isDirectory(dir)) {
- reporter.error("Not a directory {}", dir);
+ reporter.error("Not a directory {0}", dir);
return false;
}
Files.walkFileTree(dir, new SimpleFileVisitor() {
@@ -311,7 +315,7 @@ public final class FileCollector implements AutoCloseable {
} else if (Files.isRegularFile(file)) {
return addFile(file);
} else {
- reporter.error("Not a file or directory {}", file);
+ reporter.error("Not a file or directory {0}", file);
return false;
}
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileLocation.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileLocation.java
index 498a9e15a4..31b0482b5a 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileLocation.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileLocation.java
@@ -145,7 +145,7 @@ public final class FileLocation {
* @throws IllegalArgumentException If the line and column are not correctly ordered
* @throws IllegalArgumentException If the start offset or length are negative
*/
- public static FileLocation location(String fileName, TextRange2d range2d) {
+ public static FileLocation range(String fileName, TextRange2d range2d) {
TextPos2d start = range2d.getStartPos();
TextPos2d end = range2d.getEndPos();
return new FileLocation(fileName,
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java
index 21f96da6d7..95e7086818 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java
@@ -27,16 +27,24 @@ class NioTextFile extends BaseCloseable implements TextFile {
private final Charset charset;
private final LanguageVersion languageVersion;
private final @Nullable String displayName;
+ private final String pathId;
+ private boolean readOnly;
- NioTextFile(Path path, Charset charset, LanguageVersion languageVersion, @Nullable String displayName) {
+ NioTextFile(Path path,
+ Charset charset,
+ LanguageVersion languageVersion,
+ @Nullable String displayName,
+ boolean readOnly) {
AssertionUtil.requireParamNotNull("path", path);
AssertionUtil.requireParamNotNull("charset", charset);
AssertionUtil.requireParamNotNull("language version", languageVersion);
this.displayName = displayName;
+ this.readOnly = readOnly;
this.path = path;
this.charset = charset;
this.languageVersion = languageVersion;
+ this.pathId = path.toAbsolutePath().toString();
}
@Override
@@ -51,17 +59,20 @@ class NioTextFile extends BaseCloseable implements TextFile {
@Override
public String getPathId() {
- return path.toAbsolutePath().toString();
+ return pathId;
}
@Override
public boolean isReadOnly() {
- return !Files.isWritable(path);
+ return readOnly || !Files.isWritable(path);
}
@Override
public void writeContents(TextFileContent content) throws IOException {
ensureOpen();
+ if (isReadOnly()) {
+ throw new ReadOnlyFileException(this);
+ }
try (BufferedWriter bw = Files.newBufferedWriter(path, charset)) {
if (content.getLineTerminator().equals(TextFileContent.NORMALIZED_LINE_TERM)) {
content.getNormalizedText().writeFully(bw);
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/ReadOnlyFileException.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/ReadOnlyFileException.java
index 28b739c21c..df27cb13c1 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/ReadOnlyFileException.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/ReadOnlyFileException.java
@@ -10,4 +10,8 @@ package net.sourceforge.pmd.lang.document;
*/
public class ReadOnlyFileException extends UnsupportedOperationException {
+ public ReadOnlyFileException(TextFile textFile) {
+ super("Read only: " + textFile.getPathId());
+ }
+
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/RootTextDocument.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/RootTextDocument.java
index 9e8ecbb3a3..873fba75c5 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/RootTextDocument.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/RootTextDocument.java
@@ -62,6 +62,11 @@ final class RootTextDocument extends BaseCloseable implements TextDocument {
backend.close();
}
+ @Override
+ public Chars getText() {
+ return content.getNormalizedText();
+ }
+
@Override
public FileLocation toLocation(TextRegion region) {
checkInRange(region, this.getLength());
@@ -69,43 +74,23 @@ final class RootTextDocument extends BaseCloseable implements TextDocument {
// We use longs to return both numbers at the same time
// This limits us to 2 billion lines or columns, which is FINE
- long bpos = positioner.lineColFromOffset(region.getStartOffset(), true);
- long epos = region.isEmpty() ? bpos
- : positioner.lineColFromOffset(region.getEndOffset(), false);
+ TextPos2d bpos = positioner.lineColFromOffset(region.getStartOffset(), true);
+ TextPos2d epos = region.isEmpty() ? bpos
+ : positioner.lineColFromOffset(region.getEndOffset(), false);
return new FileLocation(
fileName,
- SourceCodePositioner.unmaskLine(bpos),
- SourceCodePositioner.unmaskCol(bpos),
- SourceCodePositioner.unmaskLine(epos),
- SourceCodePositioner.unmaskCol(epos),
+ bpos.getLine(),
+ bpos.getColumn(),
+ epos.getLine(),
+ epos.getColumn(),
region
);
}
- @Override
- public int offsetAtLineColumn(int line, int column) {
- SourceCodePositioner positioner = content.getPositioner();
- return positioner.offsetFromLineColumn(line, column);
- }
-
- @Override
- public boolean isInRange(TextPos2d textPos2d) {
- if (textPos2d.getLine() <= content.getPositioner().getNumLines()) {
- int maxColumn = content.getPositioner().offsetOfEndOfLine(textPos2d.getLine());
- return textPos2d.getColumn()
- < content.getPositioner().columnFromOffset(textPos2d.getLine(), maxColumn);
- }
- return false;
- }
-
@Override
public TextPos2d lineColumnAtOffset(int offset, boolean inclusive) {
- long longPos = content.getPositioner().lineColFromOffset(offset, inclusive);
- return TextPos2d.pos2d(
- SourceCodePositioner.unmaskLine(longPos),
- SourceCodePositioner.unmaskCol(longPos)
- );
+ return content.getPositioner().lineColFromOffset(offset, inclusive);
}
@Override
@@ -130,8 +115,8 @@ final class RootTextDocument extends BaseCloseable implements TextDocument {
}
@Override
- public TextFileContent getContent() {
- return content;
+ public long getCheckSum() {
+ return content.getCheckSum();
}
@Override
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/SourceCodePositioner.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/SourceCodePositioner.java
index 56ef19e26f..cf8caab34c 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/SourceCodePositioner.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/SourceCodePositioner.java
@@ -37,37 +37,24 @@ final class SourceCodePositioner {
return lineOffsets;
}
- long lineColFromOffset(int offset, boolean inclusive) {
+ TextPos2d lineColFromOffset(int offset, boolean inclusive) {
AssertionUtil.requireInInclusiveRange("offset", offset, 0, sourceCodeLength);
int line = searchLineOffset(offset);
int lineIdx = line - 1; // zero-based
- if (offset == lineOffsets[lineIdx] && !inclusive) {
+ if (lineIdx != 0 && offset == lineOffsets[lineIdx] && !inclusive) {
// we're precisely on the start of a line
// if inclusive, prefer the position at the end of the previous line
// This is a subtlety that the other methods for offset -> line do not
// handle. This is because an offset may be interpreted as the index
// of a character, or the caret position between two characters. This
// is relevant when building text regions, to respect inclusivity, etc.
- return maskLineCol(lineIdx, getLastColumnOfLine(lineIdx));
+ return TextPos2d.pos2d(lineIdx, getLastColumnOfLine(lineIdx));
}
- return maskLineCol(line, 1 + offset - lineOffsets[lineIdx]);
- }
-
- // test only
- static long maskLineCol(int line, int col) {
- return (long) line << 32 | (long) col;
- }
-
- static int unmaskLine(long lineCol) {
- return (int) (lineCol >> 32);
- }
-
- static int unmaskCol(long lineCol) {
- return (int) lineCol;
+ return TextPos2d.pos2d(line, 1 + offset - lineOffsets[lineIdx]);
}
/**
@@ -157,7 +144,8 @@ final class SourceCodePositioner {
*/
public int offsetOfEndOfLine(final int line) {
if (!isValidLine(line)) {
- throw new IndexOutOfBoundsException(line + " is not a valid line number, expected at most " + lineOffsets.length);
+ throw new IndexOutOfBoundsException(
+ line + " is not a valid line number, expected at most " + lineOffsets.length);
}
return lineOffsets[line];
@@ -187,11 +175,17 @@ final class SourceCodePositioner {
}
private int getLastColumnOfLine(int line) {
- return 1 + lineOffsets[line] - lineOffsets[line - 1];
+ if (line == 0) {
+ return 1 + lineOffsets[line];
+ } else {
+ return 1 + lineOffsets[line] - lineOffsets[line - 1];
+ }
}
/**
* Builds a new positioner for the given char sequence.
+ * The char sequence should have its newline delimiters normalized
+ * to {@link TextFileContent#NORMALIZED_LINE_TERM}.
* The char sequence should not change state (eg a {@link StringBuilder})
* after construction, otherwise this positioner becomes unreliable.
*
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextDocument.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextDocument.java
index 1523d05356..6bfc817b09 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextDocument.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextDocument.java
@@ -4,8 +4,6 @@
package net.sourceforge.pmd.lang.document;
-import static net.sourceforge.pmd.lang.document.RootTextDocument.checkInRange;
-
import java.io.Closeable;
import java.io.IOException;
import java.io.Reader;
@@ -30,6 +28,47 @@ import net.sourceforge.pmd.util.datasource.DataSource;
* from a text document. Exposing it here could lead to files being written
* to from within rules, while we want to eventually build an API that allows
* file edition based on AST manipulation.
+ *
+ * Coordinates in TextDocument
+ *
+ * This interface is an abstraction over a piece of text, which might not
+ * correspond to the backing source file. This allows the document to
+ * be a view on a piece of a larger document (eg, a Javadoc comment, or
+ * a string in which a language is injected). Another use case is to perform
+ * escape translation, while preserving the line breaks of the original source.
+ *
+ * This complicates addressing within a text document. To explain it,
+ * consider that there is always *one* text document that corresponds to
+ * the backing text file, which we call the root text document.
+ * Logical documents built on top of it are called views.
+ *
+ * Text documents use offsets and {@link TextRegion} to address their
+ * contents. These are always relative to the {@linkplain #getText() text} of
+ * the document. Line and column information are provided by {@link FileLocation}
+ * (see {@link #toLocation(TextRegion)}), and are always absolute (ie,
+ * represent actual source lines in the file).
+ *
+ *
For instance, say you have the following file (and root text document):
+ *
{@code
+ * l1
+ * l2 (* comment *)
+ * l3
+ * }
+ * and you create a view for just the section {@code (* comment *)}.
+ * Then, that view's offset 0 (start of the document) will map
+ * to the {@code (} character, while the root document's offset 0 maps
+ * to the start of {@code l1}. When calling {@code toLocation(caretAt(0))},
+ * the view will however return {@code line 2, column 4}, ie, a line/column
+ * that can be found when inspecting the file.
+ *
+ * To reduce the potential for mistakes, views do not provide access
+ * to their underlying text document. That way, nodes only have access
+ * to a single document, and their offsets can be assumed to be in the
+ * coordinate system of that document.
+ *
+ *
This interface does not provide a way to obtain line/column
+ * coordinates that are relative to a view's coordinate system. This
+ * would complicate the construction of views significantly.
*/
public interface TextDocument extends Closeable {
// todo logical sub-documents, to support embedded languages
@@ -38,25 +77,6 @@ public interface TextDocument extends Closeable {
// todo text edition (there are some reverted commits in the branch
// with part of this, including a lot of tests)
- /*
- Summary of different coordinate systems:
- Coordinate system: Line/column Offset
- ==============================================================
- Position: TextPos2d int >= 0
- Range: TextRange2d TextRegion
-
- (FileLocation is similar to TextRange2d in terms of position info)
-
- Conversions:
- line/column -> offset: offsetAtLineColumn
- offset -> line/column: lineColumnAtOffset
- Range conversions:
- TextRegion -> TextRange2d: toRegion
- TextRange2d -> TextRegion: toRange2d
-
- TextRegion -> FileLocation: toLocation
- TextRange2d -> FileLocation: toLocation
- */
/**
* Returns the language version that should be used to parse this file.
@@ -82,9 +102,7 @@ public interface TextDocument extends Closeable {
*
* @see TextFileContent#getNormalizedText()
*/
- default Chars getText() {
- return getContent().getNormalizedText();
- }
+ Chars getText();
/**
* Returns a region of the {@linkplain #getText() text} as a character sequence.
@@ -93,9 +111,11 @@ public interface TextDocument extends Closeable {
/**
- * Returns the current contents of the text file. See also {@link #getText()}.
+ * Returns a checksum for the contents of the file.
+ *
+ * @see TextFileContent#getCheckSum()
*/
- TextFileContent getContent();
+ long getCheckSum();
/**
* Returns a reader over the text of this document.
@@ -137,74 +157,19 @@ public interface TextDocument extends Closeable {
* the line/column information for both start and end offset of
* the region.
*
- * @return A new file position
+ * @param region A region, in the coordinate system of this document
+ *
+ * @return A new file position, with absolute coordinates
*
* @throws IndexOutOfBoundsException If the argument is not a valid region in this document
*/
FileLocation toLocation(TextRegion region);
- /**
- * Turn a text region into a {@link FileLocation}. The file name is
- * the display name of this document.
- *
- * @return A new file position
- *
- * @throws IndexOutOfBoundsException If the argument is not a valid region in this document
- */
- default FileLocation toLocation(TextRange2d range) {
- int startOffset = offsetAtLineColumn(range.getEndPos());
- if (startOffset < 0) {
- throw new IndexOutOfBoundsException("Region out of bounds: " + range.displayString());
- }
- TextRegion region = TextRegion.caretAt(startOffset);
- checkInRange(region, this.getLength());
- return FileLocation.location(getDisplayName(), range);
- }
-
- /**
- * Turn a text region to a {@link TextRange2d}.
- */
- default TextRange2d toRange2d(TextRegion region) {
- TextPos2d start = lineColumnAtOffset(region.getStartOffset(), true);
- TextPos2d end = lineColumnAtOffset(region.getEndOffset(), false);
- return TextRange2d.range2d(start, end);
- }
-
- /**
- * Turn a {@link TextRange2d} into a {@link TextRegion}.
- */
- default TextRegion toRegion(TextRange2d region) {
- return TextRegion.fromBothOffsets(
- offsetAtLineColumn(region.getStartPos()),
- offsetAtLineColumn(region.getEndPos())
- );
- }
-
-
- /**
- * Returns the offset at the given line and column number.
- *
- * @param line Line number (1-based)
- * @param column Column number (1-based)
- *
- * @return an offset (0-based)
- */
- int offsetAtLineColumn(int line, int column);
-
- /**
- * Returns true if the position is valid in this document.
- */
- boolean isInRange(TextPos2d textPos2d);
-
- /**
- * Returns the offset at the line and number.
- */
- default int offsetAtLineColumn(TextPos2d pos2d) {
- return offsetAtLineColumn(pos2d.getLine(), pos2d.getColumn());
- }
/**
* Returns the line and column at the given offset (inclusive).
+ * Note that the line/column cannot be converted back. They are
+ * absolute in the coordinate system of the original document.
*
* @param offset A source offset (0-based), can range in {@code [0, length]}.
*
@@ -215,20 +180,21 @@ public interface TextDocument extends Closeable {
}
/**
- * Returns the line and column at the given offset (inclusive).
+ * Returns the line and column at the given offset.
*
* @param offset A source offset (0-based), can range in {@code [0, length]}.
* @param inclusive If the offset falls right after a line terminator,
* two behaviours are possible. If the parameter is true,
- * choose the position at the start of the next line.
- * Otherwise choose the offset at the end of the line.
+ * choose the position at the start of the next line,
+ * otherwise choose the position at the end of the line.
+ *
+ * @return A position, in the coordinate system of the root document
*
* @throws IndexOutOfBoundsException if the offset is out of bounds
*/
TextPos2d lineColumnAtOffset(int offset, boolean inclusive);
-
/**
* Closing a document closes the underlying {@link TextFile}.
* New editors cannot be produced after that, and the document otherwise
@@ -243,7 +209,17 @@ public interface TextDocument extends Closeable {
@Override
void close() throws IOException;
-
+ /**
+ * Create a new text document for the given text file. The document's
+ * coordinate system is the same as the original text file.
+ *
+ * @param textFile A text file
+ *
+ * @return A new text document
+ *
+ * @throws IOException If the file cannot be read ({@link TextFile#readContents()})
+ * @throws NullPointerException If the parameter is null
+ */
static TextDocument create(TextFile textFile) throws IOException {
return new RootTextDocument(textFile);
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java
index 5b7c898eef..6e4a26b1f1 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFile.java
@@ -14,7 +14,6 @@ import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.file.Path;
-import org.apache.commons.io.IOUtils;
import org.checkerframework.checker.nullness.qual.NonNull;
import net.sourceforge.pmd.PMDConfiguration;
@@ -25,6 +24,7 @@ import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.document.TextFileBuilder.ForCharSeq;
import net.sourceforge.pmd.lang.document.TextFileBuilder.ForNio;
import net.sourceforge.pmd.lang.document.TextFileBuilder.ForReader;
+import net.sourceforge.pmd.util.IOUtil;
import net.sourceforge.pmd.util.datasource.DataSource;
/**
@@ -109,7 +109,7 @@ public interface TextFile extends Closeable {
* @throws ReadOnlyFileException If this text source is read-only
*/
default void writeContents(TextFileContent content) throws IOException {
- throw new ReadOnlyFileException();
+ throw new ReadOnlyFileException(this);
}
@@ -133,6 +133,18 @@ public interface TextFile extends Closeable {
@Override
void close() throws IOException;
+
+ /**
+ * Text file equality is implementation-defined. The only constraint
+ * is that equal text files should have equal path IDs (and the usual
+ * properties mandated by {@link Object#equals(Object)}).
+ */
+ // currently:
+ // - Path-based TextFiles compare their path for equality, where the path is not normalized.
+ // - Reader- and String-based TextFiles use identity semantics.
+ @Override
+ boolean equals(Object o);
+
// factory methods
/**
@@ -212,7 +224,24 @@ public interface TextFile extends Closeable {
*
* @throws NullPointerException If any parameter is null
*/
- static TextFileBuilder forReader(Reader reader, String pathId, LanguageVersion languageVersion) {
+ static TextFile forReader(Reader reader, String pathId, LanguageVersion languageVersion) {
+ return builderForReader(reader, pathId, languageVersion).build();
+ }
+
+ /**
+ * Returns a read-only builder reading from a reader.
+ * The reader is first read when {@link TextFile#readContents()} is first
+ * called, and is closed when that method exits. Note that this may
+ * only be called once, afterwards, {@link TextFile#readContents()} will
+ * throw an {@link IOException}.
+ *
+ * @param reader Text of the file
+ * @param pathId File name to use as path id
+ * @param languageVersion Language version
+ *
+ * @throws NullPointerException If any parameter is null
+ */
+ static TextFileBuilder builderForReader(Reader reader, String pathId, LanguageVersion languageVersion) {
return new ForReader(languageVersion, reader, pathId);
}
@@ -227,16 +256,21 @@ public interface TextFile extends Closeable {
@Deprecated
@DeprecatedUntil700
static TextFile dataSourceCompat(DataSource ds, PMDConfiguration config) {
+ String pathId = ds.getNiceFileName(false, null);
+ LanguageVersion languageVersion = config.getLanguageVersionOfFile(pathId);
+ if (languageVersion == null) {
+ throw new NullPointerException("no language version detected for " + pathId);
+ }
class DataSourceTextFile extends BaseCloseable implements TextFile {
@Override
public @NonNull LanguageVersion getLanguageVersion() {
- return config.getLanguageVersionOfFile(getPathId());
+ return languageVersion;
}
@Override
public String getPathId() {
- return ds.getNiceFileName(false, null);
+ return pathId;
}
@Override
@@ -249,7 +283,7 @@ public interface TextFile extends Closeable {
ensureOpen();
try (InputStream is = ds.getInputStream();
Reader reader = new BufferedReader(new InputStreamReader(is, config.getSourceEncoding()))) {
- String contents = IOUtils.toString(reader);
+ String contents = IOUtil.readToString(reader);
return TextFileContent.fromCharSeq(contents);
}
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileBuilder.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileBuilder.java
index 2c1e066f10..f240b51b81 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileBuilder.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileBuilder.java
@@ -26,10 +26,41 @@ public abstract class TextFileBuilder {
this.languageVersion = AssertionUtil.requireParamNotNull("language version", languageVersion);
}
+ /**
+ * Specify that the built file is read only. Some text files are
+ * always read-only.
+ *
+ * @return This builder
+ */
+ public TextFileBuilder asReadOnly() {
+ // default is appropriate if the file type is always read-only
+ return this;
+ }
+
+
+ /**
+ * Sets a custom display name for the new file. If null, or this is
+ * never called, the display name defaults to the path ID.
+ *
+ * @param displayName A display name
+ *
+ * @return This builder
+ */
+ public TextFileBuilder withDisplayName(@Nullable String displayName) {
+ this.displayName = displayName;
+ return this;
+ }
+
+ /**
+ * Creates and returns the new text file.
+ */
+ public abstract TextFile build();
+
static class ForNio extends TextFileBuilder {
private final Path path;
private final Charset charset;
+ private boolean readOnly = false;
ForNio(LanguageVersion languageVersion, Path path, Charset charset) {
super(languageVersion);
@@ -37,9 +68,15 @@ public abstract class TextFileBuilder {
this.charset = AssertionUtil.requireParamNotNull("charset", charset);
}
+ @Override
+ public TextFileBuilder asReadOnly() {
+ readOnly = true;
+ return this;
+ }
+
@Override
public TextFile build() {
- return new NioTextFile(path, charset, languageVersion, displayName);
+ return new NioTextFile(path, charset, languageVersion, displayName, readOnly);
}
}
@@ -77,22 +114,4 @@ public abstract class TextFileBuilder {
}
}
-
- /**
- * Sets a custom display name for the new file. If null, or this is
- * never called, the display name defaults to the path ID.
- *
- * @param displayName A display name
- *
- * @return This builder
- */
- public TextFileBuilder withDisplayName(@Nullable String displayName) {
- this.displayName = displayName;
- return this;
- }
-
- /**
- * Creates and returns the new text file.
- */
- public abstract TextFile build();
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileContent.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileContent.java
index 84b3a86c2f..0219448387 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileContent.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileContent.java
@@ -20,28 +20,33 @@ import java.util.zip.Adler32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Checksum;
-import org.apache.commons.io.ByteOrderMark;
-import org.apache.commons.io.IOUtils;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
+import net.sourceforge.pmd.util.IOUtil;
+
/**
* Contents of a text file.
*/
public final class TextFileContent {
+ // the three line terminators we handle.
+ private static final String CRLF = "\r\n";
+ private static final String LF = "\n";
+ private static final String CR = "\r";
+
/**
* The normalized line ending used to replace platform-specific
* line endings in the {@linkplain #getNormalizedText() normalized text}.
*/
- public static final String NORMALIZED_LINE_TERM = "\n";
+ public static final String NORMALIZED_LINE_TERM = LF;
/** The normalized line ending as a char. */
public static final char NORMALIZED_LINE_TERM_CHAR = '\n';
private static final int DEFAULT_BUFSIZE = 8192;
- private static final Pattern NEWLINE_PATTERN = Pattern.compile("\r\n|\n");
+ private static final Pattern NEWLINE_PATTERN = Pattern.compile("\r\n?|\n");
private static final String FALLBACK_LINESEP = System.lineSeparator();
private final Chars cdata;
@@ -61,9 +66,10 @@ public final class TextFileContent {
* The text of the file, with the following normalizations:
*
* - Line endings are normalized to {@value NORMALIZED_LINE_TERM}.
- * For this purpose, a line ending is either {@code \r\n} or {@code \n}
- * (CRLF or LF), not the full range of unicode line endings. This is
- * consistent with {@link BufferedReader#readLine()} for example.
+ * For this purpose, a line ending is either {@code \r}, {@code \r\n}
+ * or {@code \n} (CR, CRLF or LF), not the full range of unicode line
+ * endings. This is consistent with {@link BufferedReader#readLine()},
+ * and the JLS, for example.
*
- An initial byte-order mark is removed, if any.
*
*/
@@ -155,7 +161,7 @@ public final class TextFileContent {
static @NonNull TextFileContent normalizeCharSeq(CharSequence text, String fallBackLineSep) {
long checksum = getCheckSum(text); // the checksum is computed on the original file
- if (text.length() > 0 && text.charAt(0) == ByteOrderMark.UTF_BOM) {
+ if (text.length() > 0 && text.charAt(0) == IOUtil.UTF_BOM) {
text = text.subSequence(1, text.length()); // skip the BOM
}
Matcher matcher = NEWLINE_PATTERN.matcher(text);
@@ -202,40 +208,57 @@ public final class TextFileContent {
int bufOffset = 0;
int nextCharToCopy = 0;
int n = input.read(cbuf);
- if (n > 0 && cbuf[0] == ByteOrderMark.UTF_BOM) {
+ if (n > 0 && cbuf[0] == IOUtil.UTF_BOM) {
nextCharToCopy = 1;
}
- while (n != IOUtils.EOF) {
- if (updateChecksum) { // if we use a checked input stream we dont need to update the checksum manually
+ while (n != IOUtil.EOF) {
+ if (updateChecksum) {
+ // if we use a checked input stream we dont need to update the checksum manually
+ // note that this checksum operates on non-normalized characters
updateChecksum(checksum, CharBuffer.wrap(cbuf, nextCharToCopy, n));
}
+ int offsetDiff = 0;
+
for (int i = nextCharToCopy; i < n; i++) {
char c = cbuf[i];
- if (afterCr && c != NORMALIZED_LINE_TERM_CHAR && i == 0) {
- // we saw a \r at the end of the last buffer, but didn't copy it
- // it's actually not followed by an \n
- result.append('\r');
- }
-
- if (c == NORMALIZED_LINE_TERM_CHAR) {
+ if (afterCr || c == NORMALIZED_LINE_TERM_CHAR) {
final String newLineTerm;
- if (afterCr) {
- newLineTerm = "\r\n";
-
+ final int newLineOffset;
+ if (afterCr && c != NORMALIZED_LINE_TERM_CHAR) {
+ // we saw a \r last iteration, but didn't copy it
+ // it's not followed by an \n
+ newLineTerm = CR;
+ newLineOffset = bufOffset + i + offsetDiff;
if (i > 0) {
- cbuf[i - 1] = '\n'; // replace the \r with a \n
- // copy up to and including the \r, which was replaced
- result.append(cbuf, nextCharToCopy, i - nextCharToCopy);
- nextCharToCopy = i + 1; // set the next char to copy to after the \n
+ cbuf[i - 1] = NORMALIZED_LINE_TERM_CHAR; // replace the \r with a \n
+ } else {
+ // The CR was trailing a buffer, so it's not in the current buffer and wasn't copied.
+ // Append a newline.
+ result.append(NORMALIZED_LINE_TERM);
}
} else {
- // just \n
- newLineTerm = NORMALIZED_LINE_TERM;
+ if (afterCr) {
+ newLineTerm = CRLF;
+
+ if (i > 0) {
+ cbuf[i - 1] = NORMALIZED_LINE_TERM_CHAR; // replace the \r with a \n
+ // copy up to and including the \r, which was replaced
+ result.append(cbuf, nextCharToCopy, i - nextCharToCopy);
+ nextCharToCopy = i + 1; // set the next char to copy to after the \n
+ }
+ // Since we're replacing a 2-char delimiter with a single char,
+ // the offset of the line needs to be adjusted.
+ offsetDiff--;
+ } else {
+ // just \n
+ newLineTerm = LF;
+ }
+ newLineOffset = bufOffset + i + offsetDiff + 1;
}
- positionerBuilder.addLineEndAtOffset(bufOffset + i + 1);
+ positionerBuilder.addLineEndAtOffset(newLineOffset);
detectedLineTerm = detectLineTerm(detectedLineTerm, newLineTerm, fallbackLineSep);
}
afterCr = c == '\r';
@@ -250,18 +273,21 @@ public final class TextFileContent {
}
nextCharToCopy = 0;
- bufOffset += n;
+ bufOffset += n + offsetDiff;
n = input.read(cbuf);
} // end while
+ if (afterCr) { // we're at EOF, so it's not followed by \n
+ result.append(NORMALIZED_LINE_TERM);
+ positionerBuilder.addLineEndAtOffset(bufOffset);
+ detectedLineTerm = detectLineTerm(detectedLineTerm, CR, fallbackLineSep);
+ }
+
if (detectedLineTerm == null) {
// no line terminator in text
detectedLineTerm = fallbackLineSep;
}
- if (afterCr) { // we're at EOF, so it's not followed by \n
- result.append('\r');
- }
return new TextFileContent(Chars.wrap(result), detectedLineTerm, checksum.getValue(), positionerBuilder.build(bufOffset));
}
@@ -272,6 +298,7 @@ public final class TextFileContent {
if (curLineTerm.equals(newLineTerm)) {
return curLineTerm;
} else {
+ // todo maybe we should report a warning
return fallback; // mixed line terminators, fallback to system default
}
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextPos2d.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextPos2d.java
index bdf767af3b..b554cd3eda 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextPos2d.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextPos2d.java
@@ -18,7 +18,7 @@ public final class TextPos2d implements Comparable {
this.line = line;
this.column = column;
- assert line > 0 && column > 0 : "Invalid position" + parThis();
+ assert line > 0 && column > 0 : "Invalid position " + toTupleString();
}
/**
@@ -44,10 +44,6 @@ public final class TextPos2d implements Comparable {
return new TextPos2d(line, column);
}
- private String parThis() {
- return "(" + this + ")";
- }
-
/** Compares the start offset, then the length of a region. */
@Override
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextRange2d.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextRange2d.java
index e30cb3e6b7..14519896a9 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextRange2d.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextRange2d.java
@@ -4,12 +4,18 @@
package net.sourceforge.pmd.lang.document;
+import java.util.Comparator;
import java.util.Objects;
/**
* A place in a text document, represented as line/column information.
*/
public final class TextRange2d implements Comparable {
+ private static final Comparator COMPARATOR =
+ Comparator.comparingInt(TextRange2d::getStartLine)
+ .thenComparingInt(TextRange2d::getStartColumn)
+ .thenComparingInt(TextRange2d::getEndLine)
+ .thenComparingInt(TextRange2d::getEndColumn);
private final int startLine;
private final int startCol;
@@ -21,6 +27,8 @@ public final class TextRange2d implements Comparable {
this.startCol = startCol;
this.endLine = endLine;
this.endCol = endCol;
+ assert startCol >= 1 && startLine >= 1 && endLine >= 1 && endCol >= 1
+ : "Not a valid range " + toDisplayStringWithColon();
}
@@ -32,9 +40,25 @@ public final class TextRange2d implements Comparable {
return TextPos2d.pos2d(endLine, endCol);
}
- public String displayString() {
- return "(" + startCol + ", " + endCol
- + ")-(" + endLine + ", " + endCol + ")";
+ public String toDisplayStringWithColon() {
+ return getStartPos().toDisplayStringWithColon() + "-"
+ + getEndPos().toDisplayStringWithColon();
+ }
+
+ public int getStartLine() {
+ return startLine;
+ }
+
+ public int getStartColumn() {
+ return startCol;
+ }
+
+ public int getEndLine() {
+ return endLine;
+ }
+
+ public int getEndColumn() {
+ return endCol;
}
public static TextRange2d range2d(TextPos2d start, TextPos2d end) {
@@ -51,11 +75,7 @@ public final class TextRange2d implements Comparable {
@Override
public int compareTo(TextRange2d o) {
- int cmp = getStartPos().compareTo(o.getStartPos());
- if (cmp != 0) {
- return cmp;
- }
- return getEndPos().compareTo(o.getEndPos());
+ return COMPARATOR.compare(this, o);
}
public boolean contains(TextRange2d range) {
@@ -86,7 +106,8 @@ public final class TextRange2d implements Comparable {
@Override
public String toString() {
- return "[" + getStartPos() + " - " + getEndPos() + ']';
+ return "!debug only! [" + getStartPos().toTupleString()
+ + " - " + getEndPos().toTupleString() + ']';
}
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextRegion.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextRegion.java
index 1411997a6a..5b87b05386 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextRegion.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextRegion.java
@@ -50,7 +50,7 @@ public final class TextRegion implements Comparable {
this.startOffset = startOffset;
this.length = length;
- assert startOffset >= 0 && length >= 0 : "Invalid region" + parThis();
+ assert startOffset >= 0 && length >= 0 : "Invalid region " + this;
}
/** 0-based, inclusive index. */
@@ -122,8 +122,8 @@ public final class TextRegion implements Comparable {
* @throws AssertionError If the parameter cannot produce a valid region
*/
public TextRegion growLeft(int delta) {
- assert (delta + length) >= 0 : "Left delta " + delta + " would produce a negative length region" + parThis();
- assert (startOffset - delta) >= 0 : "Left delta " + delta + " would produce a region that starts before zero" + parThis();
+ assert delta + length >= 0 : "Left delta " + delta + " would produce a negative length region " + parThis();
+ assert startOffset - delta >= 0 : "Left delta " + delta + " would produce a region that starts before zero " + parThis();
return new TextRegion(startOffset - delta, delta + length);
}
@@ -135,7 +135,7 @@ public final class TextRegion implements Comparable {
* @throws AssertionError If the delta is negative and less than the length of this region
*/
public TextRegion growRight(int delta) {
- assert (delta + length) >= 0 : "Right delta " + delta + " would produce a negative length region" + parThis();
+ assert delta + length >= 0 : "Right delta " + delta + " would produce a negative length region " + parThis();
return new TextRegion(startOffset, delta + length);
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/rule/xpath/internal/AstElementNode.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/rule/xpath/internal/AstElementNode.java
index 1a83310247..847b96175a 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/rule/xpath/internal/AstElementNode.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/rule/xpath/internal/AstElementNode.java
@@ -52,7 +52,7 @@ public final class AstElementNode extends BaseNodeInfo implements SiblingCountin
BaseNodeInfo parent,
Node wrappedNode,
Configuration configuration) {
- super(Type.ELEMENT, configuration.getNamePool(), wrappedNode.getXPathNodeName(), parent);
+ super(determineType(wrappedNode), configuration.getNamePool(), wrappedNode.getXPathNodeName(), parent);
this.treeInfo = document;
this.wrappedNode = wrappedNode;
@@ -65,6 +65,19 @@ public final class AstElementNode extends BaseNodeInfo implements SiblingCountin
}
}
+ private static int determineType(Node node) {
+ // As of PMD 6.48.0, only the experimental HTML module uses this naming
+ // convention to identify non-element nodes.
+ // TODO PMD 7: maybe generalize this to other languages
+ String name = node.getXPathNodeName();
+ if ("#text".equals(name)) {
+ return Type.TEXT;
+ } else if ("#comment".equals(name)) {
+ return Type.COMMENT;
+ }
+ return Type.ELEMENT;
+ }
+
public Map makeAttributes(Node wrappedNode) {
Map atts = new HashMap<>();
Iterator it = wrappedNode.getXPathAttributesIterator();
@@ -179,6 +192,10 @@ public final class AstElementNode extends BaseNodeInfo implements SiblingCountin
@Override
public CharSequence getStringValueCS() {
+ if (getNodeKind() == Type.TEXT || getNodeKind() == Type.COMMENT) {
+ return getUnderlyingNode().getImage();
+ }
+
// https://www.w3.org/TR/xpath-datamodel-31/#ElementNode
// The string-value property of an Element Node must be the
// concatenation of the string-values of all its Text Node
@@ -187,6 +204,7 @@ public final class AstElementNode extends BaseNodeInfo implements SiblingCountin
// Since we represent all our Nodes as elements, there are no
// text nodes
+ // TODO: for some languages like html we have text nodes
return "";
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java
index cc164d07b4..4a79f9e516 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java
@@ -24,6 +24,7 @@ import net.sourceforge.pmd.lang.ast.Parser;
import net.sourceforge.pmd.lang.ast.Parser.ParserTask;
import net.sourceforge.pmd.lang.ast.RootNode;
import net.sourceforge.pmd.lang.ast.SemanticErrorReporter;
+import net.sourceforge.pmd.lang.ast.SemanticException;
import net.sourceforge.pmd.lang.document.TextDocument;
import net.sourceforge.pmd.lang.document.TextFile;
import net.sourceforge.pmd.reporting.FileAnalysisListener;
@@ -124,9 +125,10 @@ abstract class PmdRunnable implements Runnable {
TextDocument textDocument,
RuleSets ruleSets) throws FileAnalysisException {
+ SemanticErrorReporter reporter = SemanticErrorReporter.reportToLogger(configuration.getReporter());
ParserTask task = new ParserTask(
textDocument,
- SemanticErrorReporter.reportToLogger(LOG),
+ reporter,
configuration.getClassLoader()
);
@@ -140,6 +142,12 @@ abstract class PmdRunnable implements Runnable {
RootNode rootNode = parse(parser, task);
+ SemanticException semanticError = reporter.getFirstError();
+ if (semanticError != null) {
+ // cause a processing error to be reported and rule analysis to be skipped
+ throw semanticError;
+ }
+
ruleSets.apply(rootNode, listener);
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/AbstractRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/AbstractRenderer.java
index 0627a50834..dabd3a99ae 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/AbstractRenderer.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/AbstractRenderer.java
@@ -7,8 +7,6 @@ package net.sourceforge.pmd.renderers;
import java.io.IOException;
import java.io.Writer;
-import org.apache.commons.io.IOUtils;
-
import net.sourceforge.pmd.PMDConfiguration;
import net.sourceforge.pmd.annotation.Experimental;
import net.sourceforge.pmd.cli.PMDParameters;
@@ -103,7 +101,7 @@ public abstract class AbstractRenderer extends AbstractPropertySource implements
} catch (IOException e) {
throw new IllegalStateException(e);
} finally {
- IOUtils.closeQuietly(writer);
+ IOUtil.closeQuietly(writer);
}
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/SarifRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/SarifRenderer.java
index c7bcd5cf6b..135858d80b 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/SarifRenderer.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/SarifRenderer.java
@@ -5,12 +5,14 @@
package net.sourceforge.pmd.renderers;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import net.sourceforge.pmd.Report;
import net.sourceforge.pmd.RuleViolation;
import net.sourceforge.pmd.renderers.internal.sarif.SarifLog;
import net.sourceforge.pmd.renderers.internal.sarif.SarifLogBuilder;
+import net.sourceforge.pmd.util.IOUtil;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -70,4 +72,9 @@ public class SarifRenderer extends AbstractIncrementingRenderer {
final String json = gson.toJson(sarifLog);
writer.write(json);
}
+
+ @Override
+ public void setReportFile(String reportFilename) {
+ this.setWriter(IOUtil.createWriter(StandardCharsets.UTF_8, reportFilename));
+ }
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/XMLRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/XMLRenderer.java
index a7ff8cbbee..566ed812b9 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/XMLRenderer.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/XMLRenderer.java
@@ -22,15 +22,14 @@ import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
-import org.apache.commons.io.output.WriterOutputStream;
import org.apache.commons.lang3.StringUtils;
-import net.sourceforge.pmd.PMD;
import net.sourceforge.pmd.PMDVersion;
import net.sourceforge.pmd.Report;
import net.sourceforge.pmd.RuleViolation;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
+import net.sourceforge.pmd.util.IOUtil;
import net.sourceforge.pmd.util.StringUtil;
/**
@@ -71,7 +70,7 @@ public class XMLRenderer extends AbstractIncrementingRenderer {
public void start() throws IOException {
String encoding = getProperty(ENCODING);
String unmarkedEncoding = toUnmarkedEncoding(encoding);
- lineSeparator = PMD.EOL.getBytes(unmarkedEncoding);
+ lineSeparator = System.lineSeparator().getBytes(unmarkedEncoding);
try {
xmlWriter.writeStartDocument(encoding, "1.0");
@@ -259,7 +258,7 @@ public class XMLRenderer extends AbstractIncrementingRenderer {
public void setWriter(final Writer writer) {
String encoding = getProperty(ENCODING);
// for backwards compatibility, create a OutputStream that writes to the writer.
- this.stream = new WriterOutputStream(writer, encoding);
+ this.stream = IOUtil.fromWriter(writer, encoding);
XMLOutputFactory outputFactory = XMLOutputFactory.newFactory();
try {
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java b/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java
deleted file mode 100644
index b6533d63c4..0000000000
--- a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleBuilder.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/**
- * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
- */
-
-package net.sourceforge.pmd.rules;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.apache.commons.lang3.StringUtils;
-import org.w3c.dom.Element;
-
-import net.sourceforge.pmd.Rule;
-import net.sourceforge.pmd.RulePriority;
-import net.sourceforge.pmd.RuleSetReference;
-import net.sourceforge.pmd.annotation.InternalApi;
-import net.sourceforge.pmd.lang.Language;
-import net.sourceforge.pmd.lang.LanguageRegistry;
-import net.sourceforge.pmd.lang.LanguageVersion;
-import net.sourceforge.pmd.properties.PropertyDescriptor;
-import net.sourceforge.pmd.util.ResourceLoader;
-
-
-/**
- * Builds a rule, validating its parameters throughout. The builder can define property descriptors, but not override
- * them. For that, use {@link RuleFactory#decorateRule(Rule, RuleSetReference, Element)}.
- *
- * @author Clément Fournier
- * @since 6.0.0
- */
-@InternalApi
-@Deprecated
-public class RuleBuilder {
-
- private List> definedProperties = new ArrayList<>();
- private String name;
- private ResourceLoader resourceLoader;
- private String clazz;
- private Language language;
- private String minimumVersion;
- private String maximumVersion;
- private String since;
- private String message;
- private String externalInfoUrl;
- private String description;
- private List examples = new ArrayList<>(1);
- private RulePriority priority;
- private boolean isDeprecated;
-
- /**
- * @deprecated Use {@link #RuleBuilder(String, ResourceLoader, String, String)} with the
- * proper {@link ResourceLoader} instead. The resource loader is used to load the
- * rule implementation class from the class path.
- */
- @Deprecated
- public RuleBuilder(String name, String clazz, String language) {
- this(name, new ResourceLoader(), clazz, language);
- }
-
- public RuleBuilder(String name, ResourceLoader resourceLoader, String clazz, String language) {
- this.name = name;
- this.resourceLoader = resourceLoader;
- language(language);
- className(clazz);
- }
-
- private void language(String languageName) {
- if (StringUtils.isBlank(languageName)) {
- // Some languages don't need the attribute because the rule's
- // constructor calls setLanguage, see e.g. AbstractJavaRule
- return;
- }
-
- LanguageRegistry registry = LanguageRegistry.PMD;
- Language lang = registry.findLanguageByTerseName(languageName);
- if (lang == null) {
- throw new IllegalArgumentException(
- "Unknown Language '" + languageName + "' for rule " + name + ", supported Languages are "
- + registry.commaSeparatedList(Language::getId)
- );
- }
- language = lang;
- }
-
- private void className(String className) {
- if (StringUtils.isBlank(className)) {
- throw new IllegalArgumentException("The 'class' field of rule can't be null, nor empty.");
- }
-
- this.clazz = className;
- }
-
- public void minimumLanguageVersion(String minimum) {
- minimumVersion = minimum;
- }
-
- public void maximumLanguageVersion(String maximum) {
- maximumVersion = maximum;
- }
-
- private void checkLanguageVersionsAreOrdered(Rule rule) {
- if (rule.getMinimumLanguageVersion() != null && rule.getMaximumLanguageVersion() != null
- && rule.getMinimumLanguageVersion().compareTo(rule.getMaximumLanguageVersion()) > 0) {
- throw new IllegalArgumentException(
- "The minimum Language Version '" + rule.getMinimumLanguageVersion().getTerseName()
- + "' must be prior to the maximum Language Version '"
- + rule.getMaximumLanguageVersion().getTerseName() + "' for Rule '" + name
- + "'; perhaps swap them around?");
- }
- }
-
- public void since(String sinceStr) {
- if (StringUtils.isNotBlank(sinceStr)) {
- since = sinceStr;
- }
- }
-
- public void externalInfoUrl(String externalInfoUrl) {
- this.externalInfoUrl = externalInfoUrl;
- }
-
- public void message(String message) {
- this.message = message;
- }
-
- public void defineProperty(PropertyDescriptor> descriptor) {
- definedProperties.add(descriptor);
- }
-
-
- public void setDeprecated(boolean deprecated) {
- isDeprecated = deprecated;
- }
-
-
- public void description(String description) {
- this.description = description;
- }
-
-
- public void addExample(String example) {
- examples.add(example);
- }
-
-
- public void priority(int priorityString) {
- this.priority = RulePriority.valueOf(priorityString);
- }
-
- // Must be loaded after rule construction to know the Language
- private void loadLanguageMinMaxVersions(Rule rule) {
-
- if (minimumVersion != null) {
- LanguageVersion minimumLanguageVersion = rule.getLanguage().getVersion(minimumVersion);
- if (minimumLanguageVersion == null) {
- throwUnknownLanguageVersionException("minimum", minimumVersion, rule.getLanguage());
- } else {
- rule.setMinimumLanguageVersion(minimumLanguageVersion);
- }
- }
-
- if (maximumVersion != null) {
- LanguageVersion maximumLanguageVersion = rule.getLanguage().getVersion(maximumVersion);
- if (maximumLanguageVersion == null) {
- throwUnknownLanguageVersionException("maximum", maximumVersion, rule.getLanguage());
- } else {
- rule.setMaximumLanguageVersion(maximumLanguageVersion);
- }
- }
-
- checkLanguageVersionsAreOrdered(rule);
- }
-
- private void throwUnknownLanguageVersionException(String minOrMax, String unknownVersion, Language lang) {
- throw new IllegalArgumentException("Unknown " + minOrMax + " Language Version '" + unknownVersion
- + "' for Language '" + lang.getTerseName()
- + "' for Rule " + name
- + "; supported Language Versions are: "
- + lang.getVersions().stream().map(LanguageVersion::getVersion).collect(Collectors.joining(", ")));
- }
-
- public Rule build() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
- Rule rule = resourceLoader.loadRuleFromClassPath(clazz);
-
- rule.setName(name);
- rule.setRuleClass(clazz);
-
- if (rule.getLanguage() == null) {
- rule.setLanguage(language);
- }
-
- loadLanguageMinMaxVersions(rule);
- rule.setSince(since);
- rule.setMessage(message);
- rule.setExternalInfoUrl(externalInfoUrl);
- rule.setDeprecated(isDeprecated);
- rule.setDescription(description);
- rule.setPriority(priority == null ? RulePriority.LOW : priority);
-
- for (String example : examples) {
- rule.addExample(example);
- }
-
- for (PropertyDescriptor> descriptor : definedProperties) {
- if (!rule.getPropertyDescriptors().contains(descriptor)) {
- rule.definePropertyDescriptor(descriptor);
- }
- }
-
- return rule;
- }
-}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleFactory.java b/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleFactory.java
index 28fcfb1005..a67cdfe4b8 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleFactory.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/rules/RuleFactory.java
@@ -1,39 +1,54 @@
-/**
+/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.rules;
-import static net.sourceforge.pmd.properties.PropertyDescriptorField.DEFAULT_VALUE;
+import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.MAXIMUM_LANGUAGE_VERSION;
+import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.MINIMUM_LANGUAGE_VERSION;
+import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.NAME;
+import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.PROPERTY_TYPE;
+import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.PROPERTY_VALUE;
+import static net.sourceforge.pmd.util.internal.xml.XmlErrorMessages.ERR__INVALID_LANG_VERSION;
+import static net.sourceforge.pmd.util.internal.xml.XmlErrorMessages.ERR__INVALID_LANG_VERSION_NO_NAMED_VERSION;
+import static net.sourceforge.pmd.util.internal.xml.XmlErrorMessages.ERR__PROPERTY_DOES_NOT_EXIST;
+import static net.sourceforge.pmd.util.internal.xml.XmlErrorMessages.IGNORED__DUPLICATE_PROPERTY_SETTER;
-import java.util.AbstractMap.SimpleEntry;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
-import java.util.List;
+import java.util.HashSet;
import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
-import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.RulePriority;
import net.sourceforge.pmd.RuleSetReference;
import net.sourceforge.pmd.annotation.InternalApi;
-import net.sourceforge.pmd.internal.DOMUtils;
+import net.sourceforge.pmd.lang.Language;
+import net.sourceforge.pmd.lang.LanguageRegistry;
+import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.lang.rule.RuleReference;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyDescriptorField;
import net.sourceforge.pmd.properties.PropertyTypeId;
+import net.sourceforge.pmd.properties.ValueParser;
import net.sourceforge.pmd.properties.builders.PropertyDescriptorExternalBuilder;
import net.sourceforge.pmd.util.ResourceLoader;
+import net.sourceforge.pmd.util.StringUtil;
+import net.sourceforge.pmd.util.internal.xml.PmdXmlReporter;
+import net.sourceforge.pmd.util.internal.xml.SchemaConstant;
+import net.sourceforge.pmd.util.internal.xml.SchemaConstants;
+import net.sourceforge.pmd.util.internal.xml.XmlErrorMessages;
+import net.sourceforge.pmd.util.internal.xml.XmlUtil;
+
+import com.github.oowekyala.ooxml.DomUtils;
+import com.github.oowekyala.ooxml.messages.XmlException;
/**
@@ -46,34 +61,8 @@ import net.sourceforge.pmd.util.ResourceLoader;
@Deprecated
public class RuleFactory {
- private static final Logger LOG = LoggerFactory.getLogger(RuleFactory.class);
-
- private static final String DEPRECATED = "deprecated";
- private static final String NAME = "name";
- private static final String MESSAGE = "message";
- private static final String EXTERNAL_INFO_URL = "externalInfoUrl";
- private static final String MINIMUM_LANGUAGE_VERSION = "minimumLanguageVersion";
- private static final String MAXIMUM_LANGUAGE_VERSION = "maximumLanguageVersion";
- private static final String SINCE = "since";
- private static final String PROPERTIES = "properties";
- private static final String PRIORITY = "priority";
- private static final String EXAMPLE = "example";
- private static final String DESCRIPTION = "description";
- private static final String PROPERTY = "property";
- private static final String CLASS = "class";
-
- private static final List REQUIRED_ATTRIBUTES = Collections.unmodifiableList(Arrays.asList(NAME, CLASS));
-
private final ResourceLoader resourceLoader;
- /**
- * @deprecated Use {@link #RuleFactory(ResourceLoader)} instead.
- */
- @Deprecated
- public RuleFactory() {
- this(new ResourceLoader());
- }
-
/**
* @param resourceLoader The resource loader to load the rule from jar
*/
@@ -87,49 +76,49 @@ public class RuleFactory {
* Declaring a property in the overriding element throws an exception (the property must exist in the referenced
* rule).
*
- * @param referencedRule Referenced rule
+ * @param referencedRule Referenced rule
* @param ruleSetReference the ruleset, where the referenced rule is defined
- * @param ruleElement Element overriding some metadata about the rule
+ * @param ruleElement Element overriding some metadata about the rule
+ * @param err Error reporter
*
* @return A rule reference to the referenced rule
*/
- public RuleReference decorateRule(Rule referencedRule, RuleSetReference ruleSetReference, Element ruleElement) {
+ public RuleReference decorateRule(Rule referencedRule, RuleSetReference ruleSetReference, Element ruleElement, PmdXmlReporter err) {
RuleReference ruleReference = new RuleReference(referencedRule, ruleSetReference);
- if (ruleElement.hasAttribute(DEPRECATED)) {
- ruleReference.setDeprecated(Boolean.parseBoolean(ruleElement.getAttribute(DEPRECATED)));
- }
- if (ruleElement.hasAttribute(NAME)) {
- ruleReference.setName(ruleElement.getAttribute(NAME));
- }
- if (ruleElement.hasAttribute(MESSAGE)) {
- ruleReference.setMessage(ruleElement.getAttribute(MESSAGE));
- }
- if (ruleElement.hasAttribute(EXTERNAL_INFO_URL)) {
- ruleReference.setExternalInfoUrl(ruleElement.getAttribute(EXTERNAL_INFO_URL));
- }
+ SchemaConstants.DEPRECATED.getAttributeOpt(ruleElement).map(Boolean::parseBoolean).ifPresent(ruleReference::setDeprecated);
+ SchemaConstants.NAME.getAttributeOpt(ruleElement).ifPresent(ruleReference::setName);
+ SchemaConstants.MESSAGE.getAttributeOpt(ruleElement).ifPresent(ruleReference::setMessage);
+ SchemaConstants.EXTERNAL_INFO_URL.getAttributeOpt(ruleElement).ifPresent(ruleReference::setExternalInfoUrl);
- for (int i = 0; i < ruleElement.getChildNodes().getLength(); i++) {
- Node node = ruleElement.getChildNodes().item(i);
- if (node.getNodeType() == Node.ELEMENT_NODE) {
- switch (node.getNodeName()) {
- case DESCRIPTION:
- ruleReference.setDescription(DOMUtils.parseTextNode(node));
- break;
- case EXAMPLE:
- ruleReference.addExample(DOMUtils.parseTextNode(node));
- break;
- case PRIORITY:
- ruleReference.setPriority(RulePriority.valueOf(Integer.parseInt(DOMUtils.parseTextNode(node))));
- break;
- case PROPERTIES:
- setPropertyValues(ruleReference, (Element) node);
- break;
- default:
- throw new IllegalArgumentException("Unexpected element <" + node.getNodeName()
- + "> encountered as child of element for Rule "
- + ruleReference.getName());
+ for (Element node : DomUtils.children(ruleElement)) {
+
+ if (SchemaConstants.DESCRIPTION.matchesElt(node)) {
+
+ ruleReference.setDescription(XmlUtil.parseTextNode(node));
+
+ } else if (SchemaConstants.EXAMPLE.matchesElt(node)) {
+
+ ruleReference.addExample(XmlUtil.parseTextNode(node));
+
+ } else if (SchemaConstants.PRIORITY.matchesElt(node)) {
+
+ RulePriority priority = parsePriority(err, node);
+ if (priority == null) {
+ priority = RulePriority.MEDIUM;
}
+ ruleReference.setPriority(priority);
+
+ } else if (SchemaConstants.PROPERTIES.matchesElt(node)) {
+
+ setPropertyValues(ruleReference, node, err);
+
+ } else {
+ err.at(node).error(
+ XmlErrorMessages.ERR__UNEXPECTED_ELEMENT_IN,
+ node.getTagName(),
+ "rule " + ruleReference.getName()
+ );
}
}
@@ -145,162 +134,191 @@ public class RuleFactory {
* @param ruleElement The rule element to parse
*
* @return A new instance of the rule described by this element
+ *
* @throws IllegalArgumentException if the element doesn't describe a valid rule.
*/
- public Rule buildRule(Element ruleElement) {
- checkRequiredAttributesArePresent(ruleElement);
-
- String name = ruleElement.getAttribute(NAME);
-
- RuleBuilder builder = new RuleBuilder(name,
- resourceLoader,
- ruleElement.getAttribute(CLASS),
- ruleElement.getAttribute("language"));
-
- if (ruleElement.hasAttribute(MINIMUM_LANGUAGE_VERSION)) {
- builder.minimumLanguageVersion(ruleElement.getAttribute(MINIMUM_LANGUAGE_VERSION));
- }
-
- if (ruleElement.hasAttribute(MAXIMUM_LANGUAGE_VERSION)) {
- builder.maximumLanguageVersion(ruleElement.getAttribute(MAXIMUM_LANGUAGE_VERSION));
- }
-
- if (ruleElement.hasAttribute(SINCE)) {
- builder.since(ruleElement.getAttribute(SINCE));
- }
-
- builder.message(ruleElement.getAttribute(MESSAGE));
- builder.externalInfoUrl(ruleElement.getAttribute(EXTERNAL_INFO_URL));
- builder.setDeprecated(hasAttributeSetTrue(ruleElement, DEPRECATED));
-
- Element propertiesElement = null;
-
- final NodeList nodeList = ruleElement.getChildNodes();
- for (int i = 0; i < nodeList.getLength(); i++) {
- Node node = nodeList.item(i);
- if (node.getNodeType() != Node.ELEMENT_NODE) {
- continue;
- }
-
- switch (node.getNodeName()) {
- case DESCRIPTION:
- builder.description(DOMUtils.parseTextNode(node));
- break;
- case EXAMPLE:
- builder.addExample(DOMUtils.parseTextNode(node));
- break;
- case PRIORITY:
- builder.priority(Integer.parseInt(DOMUtils.parseTextNode(node).trim()));
- break;
- case PROPERTIES:
- parsePropertiesForDefinitions(builder, node);
- propertiesElement = (Element) node;
- break;
- default:
- throw new IllegalArgumentException("Unexpected element <" + node.getNodeName()
- + "> encountered as child of element for Rule "
- + name);
- }
- }
+ public Rule buildRule(Element ruleElement, PmdXmlReporter err) {
Rule rule;
try {
- rule = builder.build();
+ String clazz = SchemaConstants.CLASS.getNonBlankAttribute(ruleElement, err);
+ rule = resourceLoader.loadRuleFromClassPath(clazz);
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
- LOG.error("Error instantiating a rule", e);
- throw new RuntimeException(e);
+ Attr node = SchemaConstants.CLASS.getAttributeNode(ruleElement);
+ throw err.at(node).error(e);
}
- if (propertiesElement != null) {
- setPropertyValues(rule, propertiesElement);
+ rule.setName(NAME.getNonBlankAttribute(ruleElement, err));
+ if (rule.getLanguage() == null) {
+ setLanguage(ruleElement, err, rule);
+ }
+ Language language = rule.getLanguage();
+ assert language != null;
+
+ rule.setMinimumLanguageVersion(getLanguageVersion(ruleElement, err, language, MINIMUM_LANGUAGE_VERSION));
+ rule.setMaximumLanguageVersion(getLanguageVersion(ruleElement, err, language, MAXIMUM_LANGUAGE_VERSION));
+ checkVersionsAreOrdered(ruleElement, err, rule);
+
+ SchemaConstants.SINCE.getAttributeOpt(ruleElement).ifPresent(rule::setSince);
+ SchemaConstants.MESSAGE.getAttributeOpt(ruleElement).ifPresent(rule::setMessage);
+ SchemaConstants.EXTERNAL_INFO_URL.getAttributeOpt(ruleElement).ifPresent(rule::setExternalInfoUrl);
+ rule.setDeprecated(SchemaConstants.DEPRECATED.getAsBooleanAttr(ruleElement, false));
+
+ for (Element node : DomUtils.children(ruleElement)) {
+ if (SchemaConstants.DESCRIPTION.matchesElt(node)) {
+
+ rule.setDescription(XmlUtil.parseTextNode(node));
+
+ } else if (SchemaConstants.EXAMPLE.matchesElt(node)) {
+
+ rule.addExample(XmlUtil.parseTextNode(node));
+
+ } else if (SchemaConstants.PRIORITY.matchesElt(node)) {
+
+ RulePriority rp = parsePriority(err, node);
+ if (rp == null) {
+ rp = RulePriority.MEDIUM;
+ }
+ rule.setPriority(rp);
+
+ } else if (SchemaConstants.PROPERTIES.matchesElt(node)) {
+
+ parsePropertiesForDefinitions(rule, node, err);
+ setPropertyValues(rule, node, err);
+
+ } else {
+ throw err.at(node).error(
+ XmlErrorMessages.ERR__UNEXPECTED_ELEMENT_IN,
+ "rule " + NAME.getAttributeOrNull(ruleElement));
+ }
}
return rule;
}
- private void checkRequiredAttributesArePresent(Element ruleElement) {
- // add an attribute name here to make it required
-
- for (String att : REQUIRED_ATTRIBUTES) {
- if (!ruleElement.hasAttribute(att)) {
- throw new IllegalArgumentException("Missing '" + att + "' attribute");
- }
+ private void checkVersionsAreOrdered(Element ruleElement, PmdXmlReporter err, Rule rule) {
+ if (rule.getMinimumLanguageVersion() != null && rule.getMaximumLanguageVersion() != null
+ && rule.getMinimumLanguageVersion().compareTo(rule.getMaximumLanguageVersion()) > 0) {
+ throw err.at(MINIMUM_LANGUAGE_VERSION.getAttributeNode(ruleElement))
+ .error(
+ XmlErrorMessages.ERR__INVALID_VERSION_RANGE,
+ rule.getMinimumLanguageVersion(),
+ rule.getMaximumLanguageVersion()
+ );
}
}
+
/**
- * Parses a properties element looking only for the values of the properties defined or overridden.
- *
- * @param propertiesNode Node to parse
- *
- * @return A map of property names to their value
+ * Parse a priority. If invalid, report it and return null.
*/
- private Map getPropertyValuesFrom(Element propertiesNode) {
- Map overriddenProperties = new HashMap<>();
-
- for (int i = 0; i < propertiesNode.getChildNodes().getLength(); i++) {
- Node node = propertiesNode.getChildNodes().item(i);
- if (node.getNodeType() == Node.ELEMENT_NODE && PROPERTY.equals(node.getNodeName())) {
- Entry overridden = getPropertyValue((Element) node);
- overriddenProperties.put(overridden.getKey(), overridden.getValue());
- }
+ public static @Nullable RulePriority parsePriority(PmdXmlReporter err, Element node) {
+ String text = XmlUtil.parseTextNode(node);
+ RulePriority rp = RulePriority.valueOfNullable(text);
+ if (rp == null) {
+ err.at(node).error(XmlErrorMessages.ERR__INVALID_PRIORITY_VALUE, text);
+ return null;
}
+ return rp;
+ }
- return overriddenProperties;
+ private LanguageVersion getLanguageVersion(Element ruleElement, PmdXmlReporter err, Language language, SchemaConstant attrName) {
+ if (attrName.hasAttribute(ruleElement)) {
+ String attrValue = attrName.getAttributeOrThrow(ruleElement, err);
+ LanguageVersion version = language.getVersion(attrValue);
+ if (version == null) {
+ String supportedVersions = language.getVersions().stream()
+ .map(LanguageVersion::getVersion)
+ .filter(it -> !it.isEmpty())
+ .map(StringUtil::inSingleQuotes)
+ .collect(Collectors.joining(", "));
+ String message = supportedVersions.isEmpty()
+ ? ERR__INVALID_LANG_VERSION_NO_NAMED_VERSION
+ : ERR__INVALID_LANG_VERSION;
+ throw err.at(attrName.getAttributeNode(ruleElement))
+ .error(
+ message,
+ attrValue,
+ language.getTerseName(),
+ supportedVersions
+ );
+ }
+ return version;
+ }
+ return null;
+ }
+
+ private void setLanguage(Element ruleElement, PmdXmlReporter err, Rule rule) {
+ String langId = SchemaConstants.LANGUAGE.getNonBlankAttribute(ruleElement, err);
+ Language lang = LanguageRegistry.findLanguageByTerseName(langId);
+ if (lang == null) {
+ Attr node = SchemaConstants.LANGUAGE.getAttributeNode(ruleElement);
+ throw err.at(node)
+ .error("Invalid language ''{0}'', possible values are {1}", langId, supportedLanguages());
+ }
+ rule.setLanguage(lang);
+ }
+
+ private @NonNull String supportedLanguages() {
+ return LanguageRegistry.getLanguages().stream().map(Language::getTerseName).map(StringUtil::inSingleQuotes).collect(Collectors.joining(", "));
}
/**
* Parses the properties node and adds property definitions to the builder. Doesn't care for value overriding, that
* will be handled after the rule instantiation.
*
- * @param builder Rule builder
+ * @param rule Rule builder
* @param propertiesNode Node to parse
+ * @param err Error reporter
*/
- private void parsePropertiesForDefinitions(RuleBuilder builder, Node propertiesNode) {
- for (int i = 0; i < propertiesNode.getChildNodes().getLength(); i++) {
- Node node = propertiesNode.getChildNodes().item(i);
- if (node.getNodeType() == Node.ELEMENT_NODE && PROPERTY.equals(node.getNodeName())
- && isPropertyDefinition((Element) node)) {
- PropertyDescriptor> descriptor = parsePropertyDefinition((Element) node);
- builder.defineProperty(descriptor);
+ private void parsePropertiesForDefinitions(Rule rule, Element propertiesNode, @NonNull PmdXmlReporter err) {
+ for (Element child : SchemaConstants.PROPERTY_ELT.getElementChildrenNamedReportOthers(propertiesNode, err)) {
+ if (isPropertyDefinition(child)) {
+ rule.definePropertyDescriptor(parsePropertyDefinition(child, err));
}
}
}
- /**
- * Gets a mapping of property name to its value from the given property element.
- *
- * @param propertyElement Property element
- *
- * @return An entry of property name to its value
- */
- private Entry getPropertyValue(Element propertyElement) {
- String name = propertyElement.getAttribute(PropertyDescriptorField.NAME.attributeName());
- return new SimpleEntry<>(name, valueFrom(propertyElement));
- }
-
/**
* Overrides the rule's properties with the values defined in the element.
*
* @param rule The rule
* @param propertiesElt The {@literal } element
*/
- private void setPropertyValues(Rule rule, Element propertiesElt) {
- Map overridden = getPropertyValuesFrom(propertiesElt);
+ private void setPropertyValues(Rule rule, Element propertiesElt, PmdXmlReporter err) {
+ Set overridden = new HashSet<>();
- for (Entry e : overridden.entrySet()) {
- PropertyDescriptor> descriptor = rule.getPropertyDescriptor(e.getKey());
- if (descriptor == null) {
- throw new IllegalArgumentException(
- "Cannot set non-existent property '" + e.getKey() + "' on Rule " + rule.getName());
+ XmlException exception = null;
+ for (Element element : SchemaConstants.PROPERTY_ELT.getElementChildrenNamedReportOthers(propertiesElt, err)) {
+ String name = SchemaConstants.NAME.getAttributeOrThrow(element, err);
+ if (!overridden.add(name)) {
+ err.at(element).warn(IGNORED__DUPLICATE_PROPERTY_SETTER, name);
+ continue;
}
- setRulePropertyCapture(rule, descriptor, e.getValue());
+ PropertyDescriptor> desc = rule.getPropertyDescriptor(name);
+ if (desc == null) {
+ // todo just warn and ignore
+ throw err.at(element).error(ERR__PROPERTY_DOES_NOT_EXIST, name, rule.getName());
+ }
+ try {
+ setRulePropertyCapture(rule, desc, element, err);
+ } catch (XmlException e) {
+ if (exception == null) {
+ exception = e;
+ } else {
+ exception.addSuppressed(e);
+ }
+ }
+ }
+ if (exception != null) {
+ throw exception;
}
}
- private void setRulePropertyCapture(Rule rule, PropertyDescriptor descriptor, String value) {
- rule.setProperty(descriptor, descriptor.valueFrom(value));
+ private void setRulePropertyCapture(Rule rule, PropertyDescriptor descriptor, Element propertyElt, PmdXmlReporter err) {
+ T value = parsePropertyValue(propertyElt, err, descriptor::valueFrom);
+ rule.setProperty(descriptor, value);
}
/**
@@ -311,66 +329,90 @@ public class RuleFactory {
* @return True if this element defines a new property, false if this is just stating a value
*/
private static boolean isPropertyDefinition(Element node) {
- return node.hasAttribute(PropertyDescriptorField.TYPE.attributeName());
+ return SchemaConstants.PROPERTY_TYPE.hasAttribute(node);
}
/**
* Parses a property definition node and returns the defined property descriptor.
*
* @param propertyElement Property node to parse
+ * @param err Error reporter
*
* @return The property descriptor
*/
- private static PropertyDescriptor> parsePropertyDefinition(Element propertyElement) {
- String typeId = propertyElement.getAttribute(PropertyDescriptorField.TYPE.attributeName());
+ private static PropertyDescriptor> parsePropertyDefinition(Element propertyElement, PmdXmlReporter err) {
+
+ String typeId = SchemaConstants.PROPERTY_TYPE.getAttributeOrThrow(propertyElement, err);
PropertyDescriptorExternalBuilder> pdFactory = PropertyTypeId.factoryFor(typeId);
if (pdFactory == null) {
- throw new IllegalArgumentException("No property descriptor factory for type: " + typeId);
+ throw err.at(PROPERTY_TYPE.getAttributeNode(propertyElement))
+ .error(
+ XmlErrorMessages.ERR__UNSUPPORTED_PROPERTY_TYPE,
+ typeId
+ );
}
+ return propertyDefCapture(propertyElement, err, pdFactory);
+ }
+
+ private static PropertyDescriptor propertyDefCapture(Element propertyElement,
+ PmdXmlReporter err,
+ PropertyDescriptorExternalBuilder factory) {
+ // TODO support constraints like numeric range
+
+ String name = SchemaConstants.NAME.getNonBlankAttributeOrThrow(propertyElement, err);
+ String description = SchemaConstants.DESCRIPTION.getNonBlankAttributeOrThrow(propertyElement, err);
+
Map values = new HashMap<>();
- NamedNodeMap atts = propertyElement.getAttributes();
+ values.put(PropertyDescriptorField.NAME, name);
+ values.put(PropertyDescriptorField.DESCRIPTION, description);
+ String defaultValue = parsePropertyValue(propertyElement, err, s -> s);
+ values.put(PropertyDescriptorField.DEFAULT_VALUE, defaultValue);
- /// populate a map of values for an individual descriptor
- for (int i = 0; i < atts.getLength(); i++) {
- Attr a = (Attr) atts.item(i);
- values.put(PropertyDescriptorField.getConstant(a.getName()), a.getValue());
- }
-
- if (StringUtils.isBlank(values.get(DEFAULT_VALUE))) {
- NodeList children = propertyElement.getElementsByTagName(DEFAULT_VALUE.attributeName());
- if (children.getLength() == 1) {
- values.put(DEFAULT_VALUE, children.item(0).getTextContent());
- } else {
- throw new IllegalArgumentException("No value defined!");
+ // populate remaining fields
+ for (Node attrNode : DomUtils.asList(propertyElement.getAttributes())) {
+ Attr attr = (Attr) attrNode;
+ PropertyDescriptorField field = PropertyDescriptorField.getConstant(attr.getName());
+ if (field == PropertyDescriptorField.NAME
+ || field == PropertyDescriptorField.DEFAULT_VALUE
+ || field == PropertyDescriptorField.DESCRIPTION) {
+ continue;
}
+ if (field == null) {
+ err.at(attr).warn(XmlErrorMessages.IGNORED__UNEXPECTED_ATTRIBUTE_IN, propertyElement.getLocalName());
+ continue;
+ }
+ values.put(field, attr.getValue());
}
- // casting is not pretty but prevents the interface from having this method
- return pdFactory.build(values);
+ try {
+ return factory.build(values);
+ } catch (IllegalArgumentException e) {
+ // builder threw, rethrow with XML location
+ throw err.at(propertyElement).error(e);
+ }
}
- /** Gets the string value from a property node. */
- private static String valueFrom(Element propertyNode) {
- String strValue = propertyNode.getAttribute(DEFAULT_VALUE.attributeName());
+ private static T parsePropertyValue(Element propertyElt, PmdXmlReporter err, ValueParser parser) {
+ @Nullable String defaultAttr = PROPERTY_VALUE.getAttributeOrNull(propertyElt);
+ if (defaultAttr != null) {
+ Attr attrNode = PROPERTY_VALUE.getAttributeNode(propertyElt);
- if (StringUtils.isNotBlank(strValue)) {
- return strValue;
- }
+ try {
+ return parser.valueOf(defaultAttr);
+ } catch (IllegalArgumentException e) {
+ throw err.at(attrNode).error(e);
+ }
- final NodeList nodeList = propertyNode.getChildNodes();
-
- for (int i = 0; i < nodeList.getLength(); i++) {
- Node node = nodeList.item(i);
- if (node.getNodeType() == Node.ELEMENT_NODE && "value".equals(node.getNodeName())) {
- return DOMUtils.parseTextNode(node);
+ } else {
+ Element child = PROPERTY_VALUE.getSingleChildIn(propertyElt, err);
+ String text = XmlUtil.parseTextNode(child);
+ try {
+ return parser.valueOf(text);
+ } catch (IllegalArgumentException e) {
+ throw err.at(child).error(e);
}
}
- return null;
- }
-
- private static boolean hasAttributeSetTrue(Element element, String attributeId) {
- return element.hasAttribute(attributeId) && "true".equalsIgnoreCase(element.getAttribute(attributeId));
}
}
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/CollectionUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/CollectionUtil.java
index 94c21f0407..6da69a4a26 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/util/CollectionUtil.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/CollectionUtil.java
@@ -18,6 +18,7 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -25,6 +26,7 @@ import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collector;
@@ -41,6 +43,7 @@ import org.pcollections.PSet;
import net.sourceforge.pmd.annotation.InternalApi;
import net.sourceforge.pmd.internal.util.AssertionUtil;
import net.sourceforge.pmd.internal.util.IteratorUtil;
+import net.sourceforge.pmd.lang.document.Chars;
/**
* Generic collection and array-related utility functions for java.util types.
@@ -273,6 +276,22 @@ public final class CollectionUtil {
return Collections.unmodifiableList(union);
}
+ public static Map mapOf(K k0, V v0) {
+ return Collections.singletonMap(k0, v0);
+ }
+
+ public static Map buildMap(Consumer