View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import java.util.ArrayList;
8   import java.util.Collection;
9   import java.util.HashMap;
10  import java.util.Iterator;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.regex.Matcher;
14  import java.util.regex.Pattern;
15  
16  /**
17   * The {@code @checkstyle} suppression comments found in one source file.
18   *
19   * <p>It reads the same three forms that the filters of {@code checks.xml}
20   * honor and turns each into a {@link SuppressionTag} with the range it
21   * influences:</p>
22   *
23   * <ul>
24   * <li>the {@code (N lines)} form covers the {@code N} lines that follow
25   * the comment, the way {@code SuppressWithNearbyCommentFilter} and
26   * {@code SuppressWithNearbyTextFilter} read it;</li>
27   * <li>a {@code disable} comment opens a range that the matching
28   * {@code enable} comment closes, or the end of the file does, the way
29   * {@code SuppressWithPlainTextCommentFilter} reads it.</li>
30   * </ul>
31   *
32   * <p>A tag inside a string or character literal is left alone, since it is
33   * data rather than a suppression a reader wrote about this file.</p>
34   *
35   * @since 1.0
36   */
37  final class Suppressions implements Iterable<SuppressionTag> {
38  
39      /**
40       * The {@code (N lines)} form, with the check name and the line count.
41       */
42      private static final Pattern NEARBY = Pattern.compile(
43          "@checkstyle (\\w+) \\((\\d+) lines?\\)"
44      );
45  
46      /**
47       * The comment that opens a {@code disable}/{@code enable} range.
48       */
49      private static final Pattern DISABLE = Pattern.compile(
50          "@checkstyle (\\w+) disable"
51      );
52  
53      /**
54       * The comment that closes a {@code disable}/{@code enable} range.
55       */
56      private static final Pattern ENABLE = Pattern.compile(
57          "@checkstyle (\\w+) enable"
58      );
59  
60      /**
61       * Text of the source file.
62       */
63      private final String text;
64  
65      /**
66       * Constructor.
67       *
68       * @param text Text of the source file
69       */
70      Suppressions(final String text) {
71          this.text = text;
72      }
73  
74      @Override
75      public Iterator<SuppressionTag> iterator() {
76          final List<String> lines = new ArrayList<>(0);
77          this.text.lines().forEach(lines::add);
78          final Collection<SuppressionTag> tags = new ArrayList<>(0);
79          final Map<String, Integer> open = new HashMap<>(0);
80          for (int idx = 0; idx < lines.size(); ++idx) {
81              final int number = idx + 1;
82              final String line = Suppressions.code(lines.get(idx));
83              Suppressions.nearby(line, number, tags);
84              final Matcher disable = Suppressions.DISABLE.matcher(line);
85              while (disable.find()) {
86                  open.putIfAbsent(disable.group(1), number);
87              }
88              final Matcher enable = Suppressions.ENABLE.matcher(line);
89              while (enable.find()) {
90                  final Integer start = open.remove(enable.group(1));
91                  if (start != null) {
92                      tags.add(new SuppressionTag(enable.group(1), start, number));
93                  }
94              }
95          }
96          for (final Map.Entry<String, Integer> entry : open.entrySet()) {
97              tags.add(
98                  new SuppressionTag(entry.getKey(), entry.getValue(), lines.size())
99              );
100         }
101         return tags.iterator();
102     }
103 
104     private static void nearby(final String line, final int number,
105         final Collection<SuppressionTag> tags) {
106         final Matcher matcher = Suppressions.NEARBY.matcher(line);
107         while (matcher.find()) {
108             tags.add(
109                 new SuppressionTag(
110                     matcher.group(1),
111                     number,
112                     number + Integer.parseInt(matcher.group(2))
113                 )
114             );
115         }
116     }
117 
118     private static String code(final String line) {
119         final StringBuilder out = new StringBuilder(line.length());
120         char quote = 0;
121         boolean escape = false;
122         for (int idx = 0; idx < line.length(); ++idx) {
123             final char chr = line.charAt(idx);
124             if (quote == 0 && (chr == '"' || chr == '\'')) {
125                 quote = chr;
126                 out.append(chr);
127             } else if (quote == 0) {
128                 out.append(chr);
129             } else if (escape) {
130                 escape = false;
131                 out.append(' ');
132             } else if (chr == '\\') {
133                 escape = true;
134                 out.append(' ');
135             } else if (chr == quote) {
136                 quote = 0;
137                 out.append(chr);
138             } else {
139                 out.append(' ');
140             }
141         }
142         return out.toString();
143     }
144 }