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

pmd / pmd / 380

29 Jan 2026 03:55PM UTC coverage: 78.964%. Remained the same
380

push

github

adangel
[doc] ADR 3: Clarify javadoc tags (#6392)

18537 of 24358 branches covered (76.1%)

Branch coverage included in aggregate %.

40391 of 50268 relevant lines covered (80.35%)

0.81 hits per line

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

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

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

7
import static java.util.stream.Collectors.toList;
8

9
import java.util.AbstractList;
10
import java.util.ArrayList;
11
import java.util.Collection;
12
import java.util.Collections;
13
import java.util.List;
14
import java.util.RandomAccess;
15

16
import org.antlr.v4.runtime.BaseErrorListener;
17
import org.antlr.v4.runtime.CharStreams;
18
import org.antlr.v4.runtime.RecognitionException;
19
import org.antlr.v4.runtime.Recognizer;
20
import org.antlr.v4.runtime.Token;
21

22
import net.sourceforge.pmd.lang.ast.LexException;
23
import net.sourceforge.pmd.lang.ast.impl.SuppressionCommentImpl;
24
import net.sourceforge.pmd.lang.document.FileLocation;
25
import net.sourceforge.pmd.lang.document.TextDocument;
26
import net.sourceforge.pmd.lang.document.TextRegion;
27
import net.sourceforge.pmd.reporting.ViolationSuppressor.SuppressionCommentWrapper;
28

29
import io.github.apexdevtools.apexparser.ApexLexer;
30
import io.github.apexdevtools.apexparser.CaseInsensitiveInputStream;
31

32
final class ApexCommentBuilder {
1✔
33
    private final TextDocument sourceCode;
34
    private final CommentInformation commentInfo;
35

36
    ApexCommentBuilder(TextDocument sourceCode, String suppressMarker) {
1✔
37
        this.sourceCode = sourceCode;
1✔
38
        this.commentInfo = extractInformationFromComments(sourceCode, suppressMarker);
1✔
39
    }
1✔
40

41
    public boolean containsComments(ASTCommentContainer commentContainer) {
42
        if (!commentContainer.hasRealLoc()) {
1!
43
            // Synthetic nodes don't have a location and can't have comments
44
            return false;
×
45
        }
46

47
        TextRegion nodeRegion = commentContainer.getTextRegion();
1✔
48

49
        // find the first comment after the start of the container node
50
        int index = Collections.binarySearch(commentInfo.allCommentsByStartIndex, nodeRegion.getStartOffset());
1✔
51

52
        // no exact hit found - this is expected: there is no comment token starting at
53
        // the very same index as the node
54
        assert index < 0 : "comment token is at the same position as non-comment token";
1!
55
        // extract "insertion point"
56
        index = ~index;
1✔
57

58
        // now check whether the next comment after the node is still inside the node
59
        if (index >= 0 && index < commentInfo.allCommentsByStartIndex.size()) {
1!
60
            int commentStartIndex = commentInfo.allCommentsByStartIndex.get(index);
1✔
61
            return nodeRegion.getStartOffset() < commentStartIndex
1!
62
                    && nodeRegion.getEndOffset() >= commentStartIndex;
1✔
63
        }
64
        return false;
1✔
65
    }
66

67
    public void addFormalComments() {
68
        for (ApexDocToken docToken : commentInfo.docTokens) {
1✔
69
            AbstractApexNode parent = docToken.nearestNode;
1✔
70
            if (parent != null) {
1✔
71
                ASTFormalComment comment = new ASTFormalComment(docToken.token);
1✔
72
                comment.calculateTextRegion(sourceCode);
1✔
73
                parent.insertChild(comment, 0);
1✔
74
            }
75
        }
1✔
76
    }
1✔
77

78
    /**
79
     * Only remembers the node, to which the comment could belong.
80
     * Since the visiting order of the nodes does not match the source order,
81
     * the nodes appearing later in the source might be visiting first.
82
     * The correct node will then be visited afterwards, and since the distance
83
     * to the comment is smaller, it overrides the remembered node.
84
     *
85
     * @param node the potential parent node, to which the comment could belong
86
     */
87
    public void buildFormalComment(AbstractApexNode node) {
88
        if (!node.hasRealLoc()) {
1✔
89
            // Synthetic nodes such as "invoke" ASTMethod for trigger bodies don't have a location in the
90
            // source code, since they are generated by the parser/compiler (see ApexTreeBuilder)
91
            return;
1✔
92
        }
93
        // find the token, that appears as close as possible before the node
94
        TextRegion nodeRegion = node.getTextRegion();
1✔
95
        for (ApexDocToken docToken : commentInfo.docTokens) {
1✔
96
            if (docToken.token.getStartIndex() > nodeRegion.getStartOffset()) {
1✔
97
                // this and all remaining tokens are after the node
98
                // so no need to check the remaining tokens.
99
                break;
1✔
100
            }
101

102
            if (docToken.nearestNode == null
1✔
103
                || nodeRegion.compareTo(docToken.nearestNode.getTextRegion()) < 0) {
1✔
104

105
                docToken.nearestNode = node;
1✔
106
            }
107
        }
1✔
108
    }
1✔
109

110
    private static CommentInformation extractInformationFromComments(TextDocument sourceCode, String suppressMarker) {
111
        String source = sourceCode.getText().toString();
1✔
112
        ApexLexer lexer = new ApexLexer(new CaseInsensitiveInputStream(CharStreams.fromString(source)));
1✔
113
        lexer.removeErrorListeners();
1✔
114
        lexer.addErrorListener(new BaseErrorListener() {
1✔
115
            @Override
116
            public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
117
                throw new LexException(line, charPositionInLine, sourceCode.getFileId(), msg, e);
×
118
            }
119
        });
120

121
        List<Token> allCommentTokens = new ArrayList<>();
1✔
122
        List<SuppressionCommentWrapper> suppressionComments = new ArrayList<>();
1✔
123
        int lastStartIndex = -1;
1✔
124
        Token token = lexer.nextToken();
1✔
125

126
        boolean checkForCommentSuppression = suppressMarker != null;
1!
127

128
        while (token.getType() != Token.EOF) {
1✔
129
            // Keep track of all comment tokens
130
            if (token.getChannel() == ApexLexer.COMMENT_CHANNEL) {
1✔
131
                assert lastStartIndex < token.getStartIndex()
1!
132
                    : "Comments should be sorted";
133
                allCommentTokens.add(token);
1✔
134
            }
135

136
            if (checkForCommentSuppression && token.getType() == ApexLexer.LINE_COMMENT) {
1!
137
                // check if it starts with the suppress marker
138
                String trimmedCommentText = token.getText().substring(2).trim();
1✔
139

140
                if (trimmedCommentText.startsWith(suppressMarker)) {
1✔
141
                    String userMessage = trimmedCommentText.substring(suppressMarker.length()).trim();
1✔
142
                    FileLocation loc = FileLocation.caret(
1✔
143
                        sourceCode.getFileId(),
1✔
144
                        token.getLine(),
1✔
145
                        token.getCharPositionInLine() + 1
1✔
146
                    );
147
                    suppressionComments.add(new SuppressionCommentImpl<>(() -> loc, userMessage));
1✔
148
                }
149
            }
150

151
            lastStartIndex = token.getStartIndex();
1✔
152
            token = lexer.nextToken();
1✔
153
        }
154

155
        return new CommentInformation(suppressionComments, allCommentTokens);
1✔
156
    }
157

158
    private static class CommentInformation {
159

160
        final Collection<SuppressionCommentWrapper> suppressionComments;
161
        final List<Integer> allCommentsByStartIndex;
162
        final List<ApexDocToken> docTokens;
163

164
        CommentInformation(Collection<SuppressionCommentWrapper> suppressMap, List<Token> allCommentTokens) {
1✔
165
            this.suppressionComments = suppressMap;
1✔
166
            this.docTokens = allCommentTokens.stream()
1✔
167
                .filter(token -> token.getType() == ApexLexer.DOC_COMMENT)
1✔
168
                .map(ApexDocToken::new)
1✔
169
                .collect(toList());
1✔
170
            this.allCommentsByStartIndex = new TokenListByStartIndex(new ArrayList<>(allCommentTokens));
1✔
171
        }
1✔
172
    }
173

174
    /**
175
     * List that maps comment tokens to their start index without copy.
176
     * This is used to implement a "binary search by key" routine which unfortunately isn't in the stdlib.
177
     *
178
     * <p>
179
     * Note that the provided token list must implement {@link RandomAccess}.
180
     */
181
    private static final class TokenListByStartIndex extends AbstractList<Integer> implements RandomAccess {
182

183
        private final List<Token> tokens;
184

185
        <T extends List<Token> & RandomAccess> TokenListByStartIndex(T tokens) {
1✔
186
            this.tokens = tokens;
1✔
187
        }
1✔
188

189
        @Override
190
        public Integer get(int index) {
191
            return tokens.get(index).getStartIndex();
1✔
192
        }
193

194
        @Override
195
        public int size() {
196
            return tokens.size();
1✔
197
        }
198
    }
199

200
    private static class ApexDocToken {
201
        AbstractApexNode nearestNode;
202
        Token token;
203

204
        ApexDocToken(Token token) {
1✔
205
            this.token = token;
1✔
206
        }
1✔
207
    }
208

209
    public Collection<SuppressionCommentWrapper> getSuppressMap() {
210
        return commentInfo.suppressionComments;
1✔
211
    }
212
}
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