• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 4515

27 Mar 2025 12:01PM UTC coverage: 77.853% (+0.06%) from 77.796%
4515

push

github

adangel
Fix #5590: [java] LiteralsFirstInComparisons with constant field (#5595)

Merge pull request #5595 from oowekyala:issue5590-literal-comparison

17580 of 23536 branches covered (74.69%)

Branch coverage included in aggregate %.

106 of 110 new or added lines in 11 files covered. (96.36%)

62 existing lines in 5 files now uncovered.

38483 of 48475 relevant lines covered (79.39%)

0.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.0
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTStringLiteral.java
1
/**
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.lang.java.ast;
6

7
import java.util.List;
8
import java.util.stream.Collectors;
9

10
import org.checkerframework.checker.nullness.qual.NonNull;
11
import org.checkerframework.checker.nullness.qual.Nullable;
12

13
import net.sourceforge.pmd.lang.document.Chars;
14
import net.sourceforge.pmd.lang.rule.xpath.NoAttribute;
15
import net.sourceforge.pmd.util.StringUtil;
16

17
/**
18
 * Represents a string literal. The image of this node is the literal as it appeared
19
 * in the source ({@link #getLiteralText()}). {@link #getConstValue()} allows to recover
20
 * the actual runtime value, by processing escapes.
21
 */
22
public final class ASTStringLiteral extends AbstractLiteral implements ASTLiteral {
1✔
23

24
    private static final String TEXTBLOCK_DELIMITER = "\"\"\"";
25

26
    private boolean isTextBlock;
27

28
    ASTStringLiteral(int id) {
29
        super(id);
1✔
30
    }
1✔
31

32

33
    // TODO deprecate / remove this
34
    // it's ambiguous whether it returns getOriginalText or getTranslatedText
35
    @Override
36
    public String getImage() {
37
        return getText().toString();
1✔
38
    }
39

40
    @Override
41
    public Chars getLiteralText() {
42
        return super.getLiteralText();
1✔
43
    }
44

45
    void setTextBlock() {
46
        this.isTextBlock = true;
1✔
47
    }
1✔
48

49
    /** Returns true if this is a text block (currently Java 13 preview feature). */
50
    public boolean isTextBlock() {
51
        return isTextBlock;
1✔
52
    }
53

54
    /** True if the constant value is empty. Does not necessarily compute the constant value. */
55
    public boolean isEmpty() {
56
        if (isTextBlock) {
1✔
57
            return getConstValue().isEmpty(); // could be a bunch of ignorable indents?
1✔
58
        } else {
59
            return getLiteralText().length() == 2; // ""
1✔
60
        }
61
    }
62

63
    /** Length of the constant value in characters. */
64
    public int length() {
65
        return getConstValue().length();
1✔
66
    }
67

68
    /**
69
     * Returns a string where non-printable characters have been escaped
70
     * using Java-like escape codes (eg \n, \t, \u005cu00a0).
71
     */
72
    //                                          ^^^^^^
73
    // this is a backslash, it's printed as \u00a0
74
    @NoAttribute
75
    public @NonNull String toPrintableString() {
76
        return StringUtil.inDoubleQuotes(StringUtil.escapeJava(getConstValue()));
1✔
77
    }
78

79
    @Override
80
    protected <P, R> R acceptVisitor(JavaVisitor<? super P, ? extends R> visitor, P data) {
81
        return visitor.visit(this, data);
1✔
82
    }
83

84

85
    /** Returns the value without delimiters and unescaped. */
86
    @Override
87
    public @NonNull String getConstValue() {
88
        return (String) super.getConstValue(); // value is cached
1✔
89
    }
90

91
    /**
92
     * @deprecated Since 7.12.0. See super method. This override is needed due to covariant return type change.
93
     */
94
    @Override
95
    @Deprecated
96
    protected @Nullable String buildConstValue() {
NEW
97
        return (String) super.buildConstValue();
×
98
    }
99

100
    static @NonNull String determineStringContent(Chars image) {
101
        Chars woDelims = image.subSequence(1, image.length() - 1);
1✔
102
        StringBuilder sb = new StringBuilder(woDelims.length());
1✔
103
        interpretEscapeSequences(woDelims, sb, false);
1✔
104
        return sb.toString();
1✔
105
    }
106

107
    static String determineTextBlockContent(Chars image) {
108
        List<Chars> lines = getContentLines(image);
1✔
109
        // remove common prefix
110
        StringUtil.trimIndentInPlace(lines);
1✔
111
        StringBuilder sb = new StringBuilder(image.length());
1✔
112
        for (int i = 0; i < lines.size(); i++) {
1✔
113
            Chars line = lines.get(i);
1✔
114
            boolean isLastLine = i == lines.size() - 1;
1✔
115
            // this might return false if the line ends with a line continuation.
116
            boolean appendNl = interpretEscapeSequences(line, sb, !isLastLine);
1✔
117
            if (appendNl) {
1✔
118
                sb.append('\n');
1✔
119
            }
120
        }
121
        return sb.toString();
1✔
122
    }
123

124
    static String determineTextBlockContent(String image) {
125
        return determineTextBlockContent(Chars.wrap(image));
1✔
126
    }
127

128
    /**
129
     * Returns the lines of the parameter minus the delimiters.
130
     */
131
    private static @NonNull List<Chars> getContentLines(Chars chars) {
132
        List<Chars> lines = chars.lineStream().collect(Collectors.toList());
1✔
133
        assert lines.size() >= 2 : "invalid text block syntax " + chars;
1!
134
        // remove first line, it's just """ and some whitespace
135
        lines = lines.subList(1, lines.size());
1✔
136

137
        // trim the """ off the last line.
138
        int lastIndex = lines.size() - 1;
1✔
139
        Chars lastLine = lines.get(lastIndex);
1✔
140
        assert lastLine.endsWith(TEXTBLOCK_DELIMITER);
1!
141
        lines.set(lastIndex, lastLine.removeSuffix(TEXTBLOCK_DELIMITER));
1✔
142

143
        return lines;
1✔
144
    }
145

146
    /**
147
     * Interpret escape sequences. This appends the interpreted contents
148
     * of 'line' into the StringBuilder. The line does not contain any
149
     * line terminators, instead, an implicit line terminator may be at
150
     * the end (parameter {@code isEndANewLine}), to interpret line
151
     * continuations.
152
     *
153
     * @param line          Source line
154
     * @param out           Output
155
     * @param isEndANewLine Whether the end of the line is a newline,
156
     *                      as in text blocks
157
     *
158
     * @return Whether a newline should be appended at the end. Returns
159
     *     false if {@code isEndANewLine} and the line ends with a backslash,
160
     *     as this is a line continuation.
161
     */
162
    // See https://docs.oracle.com/javase/specs/jls/se17/html/jls-3.html#jls-EscapeSequence
163
    private static boolean interpretEscapeSequences(Chars line, StringBuilder out, boolean isEndANewLine) {
164
        // we need to interpret everything in one pass, so regex replacement is inappropriate
165
        int appended = 0;
1✔
166
        int i = 0;
1✔
167
        while (i < line.length()) {
1✔
168
            char c = line.charAt(i);
1✔
169
            if (c != '\\') {
1✔
170
                i++;
1✔
171
                continue;
1✔
172
            }
173
            if (i + 1 == line.length()) {
1✔
174
                // the last character of the line is a backslash
175
                if (isEndANewLine) {
1✔
176
                    // then this is a line continuation
177
                    line.appendChars(out, appended, i);
1✔
178
                    return false; // shouldn't append newline
1✔
179
                }
180
                // otherwise we'll append the backslash when exiting the loop
181
                break;
182
            }
183
            char cnext = line.charAt(i + 1);
1✔
184
            switch (cnext) {
1✔
185
            case '\\':
186
            case 'n':
187
            case 't':
188
            case 'b':
189
            case 'r':
190
            case 'f':
191
            case 's':
192
            case '"':
193
            case '\'':
194
                // append up to and not including backslash
195
                line.appendChars(out, appended, i);
1✔
196
                // append the translation
197
                out.append(translateBackslashEscape(cnext));
1✔
198
                // next time, start appending after the char
199
                i += 2;
1✔
200
                appended = i;
1✔
201
                continue;
1✔
202
            // octal digits
203
            case '0':
204
            case '1':
205
            case '2':
206
            case '3':
207
            case '4':
208
            case '5':
209
            case '6':
210
            case '7':
211
                // append up to and not including backslash
212
                line.appendChars(out, appended, i);
1✔
213
                i = translateOctalEscape(line, i + 1, out);
1✔
214
                appended = i;
1✔
215
                continue;
1✔
216
            default:
217
                // unknown escape - do nothing - it stays
218
                i++;
1✔
219
                break;
220
            }
221
        }
1✔
222

223
        if (appended < line.length()) {
1✔
224
            // append until the end
225
            line.appendChars(out, appended, line.length());
1✔
226
        }
227
        return isEndANewLine;
1✔
228
    }
229

230
    private static char translateBackslashEscape(char c) {
231
        switch (c) {
1!
232
        case '\\': return '\\';
1✔
233
        case 'n': return '\n';
1✔
234
        case 't': return '\t';
1✔
235
        case 'b': return '\b';
1✔
236
        case 'r': return '\r';
1✔
237
        case 'f': return '\f';
1✔
238
        case 's': return ' ';
1✔
239
        case '"': return '"';
1✔
240
        case '\'': return '\'';
1✔
241
        default:
242
            throw new IllegalArgumentException("Not a valid escape \\" + c);
×
243
        }
244
    }
245

246
    private static int translateOctalEscape(Chars src, final int firstDigitIndex, StringBuilder sb) {
247
        int i = firstDigitIndex;
1✔
248
        int result = src.charAt(i) - '0';
1✔
249
        i++;
1✔
250
        if (src.length() > i && isOctalDigit(src.charAt(i))) {
1!
251
            result = 8 * result + src.charAt(i) - '0';
1✔
252
            i++;
1✔
253
            if (src.length() > i && isOctalDigit(src.charAt(i))) {
1✔
254
                result = 8 * result + src.charAt(i) - '0';
1✔
255
                i++;
1✔
256
            }
257
        }
258
        sb.append((char) result);
1✔
259
        return i;
1✔
260
    }
261

262
    private static boolean isOctalDigit(char c) {
263
        return c >= '0' && c <= '7';
1!
264
    }
265
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc