View Javadoc
1   /*
2    * The MIT License (MIT)
3    *
4    * Copyright (c) 2013-2026 The Coveralls Maven Plugin Project Contributors:
5    *     https://github.com/hazendaz/coveralls-maven-plugin/graphs/contributors
6    *
7    * Permission is hereby granted, free of charge, to any person obtaining a copy
8    * of this software and associated documentation files (the "Software"), to deal
9    * in the Software without restriction, including without limitation the rights
10   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11   * copies of the Software, and to permit persons to whom the Software is
12   * furnished to do so, subject to the following conditions:
13   *
14   * The above copyright notice and this permission notice shall be included in
15   * all copies or substantial portions of the Software.
16   *
17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23   * THE SOFTWARE.
24   */
25  package org.eluder.coveralls.maven.plugin.domain;
26  
27  import com.fasterxml.jackson.annotation.JsonIgnore;
28  import com.fasterxml.jackson.annotation.JsonProperty;
29  
30  import java.util.ArrayList;
31  import java.util.Collections;
32  import java.util.List;
33  import java.util.Objects;
34  import java.util.regex.Pattern;
35  
36  /**
37   * The Class Source.
38   */
39  public final class Source implements JsonObject {
40  
41      /** The Constant serialVersionUID. */
42      private static final long serialVersionUID = 1L;
43  
44      /** The Constant NEWLINE. */
45      private static final Pattern NEWLINE = Pattern.compile("\r\n|\r|\n");
46  
47      // XXX #45: cannot use identifier due to unfetchable source files
48      // /** The Constant CLASSIFIER_SEPARATOR. */
49      // private static final String CLASSIFIER_SEPARATOR = "#";
50  
51      /** The name. */
52      String name;
53  
54      /** The digest. */
55      String digest;
56  
57      /** The coverage. */
58      Integer[] coverage;
59  
60      /** The branches. */
61      List<Branch> branches;
62  
63      /** The classifier. */
64      String classifier;
65  
66      /**
67       * Instantiates a new source.
68       *
69       * @param name
70       *            the name
71       * @param source
72       *            the source
73       * @param digest
74       *            the digest
75       */
76      public Source(final String name, final String source, final String digest) {
77          this(name, Source.getLines(source), digest, null);
78      }
79  
80      /**
81       * Instantiates a new source.
82       *
83       * @param name
84       *            the name
85       * @param lines
86       *            the lines
87       * @param digest
88       *            the digest
89       * @param classifier
90       *            the classifier
91       */
92      public Source(final String name, final int lines, final String digest, final String classifier) {
93          this.name = name;
94          this.digest = digest;
95          this.coverage = new Integer[lines];
96          this.classifier = classifier;
97          this.branches = new ArrayList<>();
98      }
99  
100     /**
101      * Gets the name.
102      *
103      * @return the name
104      */
105     @JsonIgnore
106     public String getName() {
107         return this.name;
108     }
109 
110     /**
111      * Gets the full name.
112      *
113      * @return the full name
114      */
115     @JsonProperty("name")
116     public String getFullName() {
117         return this.name;
118 
119         // XXX #45: cannot use identifier due to unfetchable source files
120         // return (classifier == null ? name : name + CLASSIFIER_SEPARATOR + classifier);
121     }
122 
123     /**
124      * Gets the digest.
125      *
126      * @return the digest
127      */
128     @JsonProperty("source_digest")
129     public String getDigest() {
130         return this.digest;
131     }
132 
133     /**
134      * Gets the coverage.
135      *
136      * @return the coverage
137      */
138     @JsonProperty("coverage")
139     public Integer[] getCoverage() {
140         return this.coverage;
141     }
142 
143     /**
144      * Gets the branches.
145      *
146      * @return the branches
147      */
148     @JsonProperty("branches")
149     public Integer[] getBranches() {
150         final List<Integer> branchesRaw = new ArrayList<>(this.branches.size() * 4);
151         for (final Branch b : this.branches) {
152             branchesRaw.add(b.getLineNumber());
153             branchesRaw.add(b.getBlockNumber());
154             branchesRaw.add(b.getBranchNumber());
155             branchesRaw.add(b.getHits());
156         }
157         return branchesRaw.toArray(new Integer[branchesRaw.size()]);
158     }
159 
160     /**
161      * Gets the branches list.
162      *
163      * @return the branches list
164      */
165     public List<Branch> getBranchesList() {
166         return Collections.unmodifiableList(this.branches);
167     }
168 
169     /**
170      * Gets the classifier.
171      *
172      * @return the classifier
173      */
174     @JsonIgnore
175     public String getClassifier() {
176         return this.classifier;
177     }
178 
179     /**
180      * Sets the classifier.
181      *
182      * @param classifier
183      *            the new classifier
184      */
185     public void setClassifier(final String classifier) {
186         this.classifier = classifier;
187     }
188 
189     /**
190      * Check line range.
191      *
192      * @param lineNumber
193      *            the line number
194      */
195     private void checkLineRange(final int lineNumber) {
196         final var index = lineNumber - 1;
197         if (index >= this.coverage.length) {
198             throw new IllegalArgumentException(
199                     "Line number " + lineNumber + " is greater than the source file " + this.name + " size");
200         }
201     }
202 
203     /**
204      * Adds the coverage.
205      *
206      * @param lineNumber
207      *            the line number
208      * @param coverage
209      *            the coverage
210      */
211     public void addCoverage(final int lineNumber, final Integer coverage) {
212         this.checkLineRange(lineNumber);
213         this.coverage[lineNumber - 1] = coverage;
214     }
215 
216     /**
217      * Adds the branch coverage.
218      *
219      * @param lineNumber
220      *            the line number
221      * @param blockNumber
222      *            the block number
223      * @param branchNumber
224      *            the branch number
225      * @param hits
226      *            the hits
227      */
228     public void addBranchCoverage(final int lineNumber, final int blockNumber, final int branchNumber, final int hits) {
229         this.addBranchCoverage(false, lineNumber, blockNumber, branchNumber, hits);
230     }
231 
232     /**
233      * Adds the branch coverage.
234      *
235      * @param merge
236      *            the merge
237      * @param lineNumber
238      *            the line number
239      * @param blockNumber
240      *            the block number
241      * @param branchNumber
242      *            the branch number
243      * @param hits
244      *            the hits
245      */
246     private void addBranchCoverage(final boolean merge, final int lineNumber, final int blockNumber,
247             final int branchNumber, final int hits) {
248         this.checkLineRange(lineNumber);
249         var hitSum = hits;
250         final var it = this.branches.listIterator();
251         while (it.hasNext()) {
252             final var b = it.next();
253             if (b.getLineNumber() == lineNumber && b.getBlockNumber() == blockNumber
254                     && b.getBranchNumber() == branchNumber) {
255                 it.remove();
256                 if (merge) {
257                     hitSum += b.getHits();
258                 }
259             }
260         }
261         this.branches.add(new Branch(lineNumber, blockNumber, branchNumber, hitSum));
262     }
263 
264     /**
265      * Merge.
266      *
267      * @param source
268      *            the source
269      *
270      * @return the source
271      */
272     public Source merge(final Source source) {
273         final var copy = new Source(this.name, this.coverage.length, this.digest, this.classifier);
274         System.arraycopy(this.coverage, 0, copy.coverage, 0, this.coverage.length);
275         copy.branches.addAll(this.branches);
276         if (copy.equals(source)) {
277             for (var i = 0; i < copy.coverage.length; i++) {
278                 if (source.coverage[i] != null) {
279                     final var base = copy.coverage[i] != null ? copy.coverage[i] : 0;
280                     copy.coverage[i] = base + source.coverage[i];
281                 }
282             }
283             for (final Branch b : source.branches) {
284                 copy.addBranchCoverage(true, b.getLineNumber(), b.getBlockNumber(), b.getBranchNumber(), b.getHits());
285             }
286         }
287         return copy;
288     }
289 
290     @Override
291     public boolean equals(final Object obj) {
292         if (!(obj instanceof Source)) {
293             return false;
294         }
295         final var other = (Source) obj;
296         return Objects.equals(this.name, other.name) && Objects.equals(this.digest, other.digest)
297                 && this.coverage.length == other.coverage.length;
298     }
299 
300     @Override
301     public int hashCode() {
302         return Objects.hash(this.name, this.digest, this.coverage.length);
303     }
304 
305     /**
306      * Gets the lines.
307      *
308      * @param source
309      *            the source
310      *
311      * @return the lines
312      */
313     private static int getLines(final String source) {
314         var lines = 1;
315         final var matcher = Source.NEWLINE.matcher(source);
316         while (matcher.find()) {
317             lines++;
318         }
319         return lines;
320     }
321 }