| 1 | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | // Copyright 2026 Egothor | |
| 3 | // Copyright 2026 Accenture | |
| 4 | package org.egothor.methodatlas.command; | |
| 5 | ||
| 6 | import java.nio.file.Path; | |
| 7 | import java.util.List; | |
| 8 | ||
| 9 | import org.egothor.methodatlas.api.CommentRedactionException; | |
| 10 | import org.egothor.methodatlas.api.CommentRedactor; | |
| 11 | ||
| 12 | /** | |
| 13 | * Applies the loaded {@link CommentRedactor}s to source text on its way to an | |
| 14 | * AI provider, failing closed. | |
| 15 | * | |
| 16 | * <p> | |
| 17 | * The first redactor whose {@link CommentRedactor#supports(Path)} accepts the | |
| 18 | * file performs the redaction. If none does, or the redactor reports it cannot | |
| 19 | * guarantee redaction, {@link RedactionAbortedException} is thrown so that no | |
| 20 | * un-redacted source can leave the process. | |
| 21 | * </p> | |
| 22 | * | |
| 23 | * <p> | |
| 24 | * Instances are immutable; thread safety is that of the wrapped redactors. | |
| 25 | * </p> | |
| 26 | * | |
| 27 | * @see CommentRedactor | |
| 28 | * @see PluginLoader#loadRedactors | |
| 29 | * @since 4.1.0 | |
| 30 | */ | |
| 31 | public final class CommentRedaction { | |
| 32 | ||
| 33 | private static final char LF = '\n'; | |
| 34 | private static final char CR = '\r'; | |
| 35 | private static final char BLANK = ' '; | |
| 36 | ||
| 37 | private final List<CommentRedactor> redactors; | |
| 38 | ||
| 39 | /** | |
| 40 | * Creates a service over the given redactors. | |
| 41 | * | |
| 42 | * @param redactors redactors in priority order; must not be {@code null} | |
| 43 | */ | |
| 44 | public CommentRedaction(List<CommentRedactor> redactors) { | |
| 45 | this.redactors = List.copyOf(redactors); | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Returns a service with no redactors, which rejects every file. | |
| 50 | * | |
| 51 | * @return a new empty instance; never {@code null} | |
| 52 | */ | |
| 53 | public static CommentRedaction none() { | |
| 54 |
1
1. none : replaced return value with null for org/egothor/methodatlas/command/CommentRedaction::none → KILLED |
return new CommentRedaction(List.of()); |
| 55 | } | |
| 56 | ||
| 57 | /** | |
| 58 | * Returns {@code source} with comments blanked by the redactor responsible | |
| 59 | * for {@code file}. | |
| 60 | * | |
| 61 | * @param file the file the text came from; {@code null} is treated as an | |
| 62 | * unknown language and rejected | |
| 63 | * @param source full source text; must not be {@code null} | |
| 64 | * @return redacted text, same length and line structure as {@code source} | |
| 65 | * @throws RedactionAbortedException if no redactor supports the file or the | |
| 66 | * redactor fails | |
| 67 | */ | |
| 68 | public String redactOrThrow(Path file, String source) { | |
| 69 |
2
1. redactOrThrow : removed conditional - replaced equality check with true → KILLED 2. redactOrThrow : removed conditional - replaced equality check with false → KILLED |
if (file == null) { |
| 70 | throw new RedactionAbortedException("-redact-comments cannot determine the language of a source " | |
| 71 | + "without a file path; refusing to send it to the AI provider."); | |
| 72 | } | |
| 73 | for (CommentRedactor redactor : redactors) { | |
| 74 |
2
1. redactOrThrow : removed conditional - replaced equality check with true → KILLED 2. redactOrThrow : removed conditional - replaced equality check with false → KILLED |
if (redactor.supports(file)) { |
| 75 |
1
1. redactOrThrow : replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::redactOrThrow → KILLED |
return redactWith(redactor, file, source); |
| 76 | } | |
| 77 | } | |
| 78 | throw new RedactionAbortedException("-redact-comments is set but no comment redactor supports " | |
| 79 | + file + " (language: " + languageOf(file) + "). -redact-comments requires a redactor " | |
| 80 | + "for every scanned language; the run is aborted so no un-redacted source reaches the " | |
| 81 | + "AI provider."); | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * Runs one redactor defensively and verifies its output, so a faulty current or | |
| 86 | * future redactor can never let un-redacted text through. | |
| 87 | * | |
| 88 | * @throws RedactionAbortedException if the redactor throws anything other than an | |
| 89 | * unrecoverable VM error, or returns text violating the redaction contract | |
| 90 | */ | |
| 91 | @SuppressWarnings("PMD.AvoidCatchingGenericException") | |
| 92 | private static String redactWith(CommentRedactor redactor, Path file, String source) { | |
| 93 | String redacted; | |
| 94 | try { | |
| 95 | redacted = redactor.redact(file, source); | |
| 96 | } catch (CommentRedactionException e) { | |
| 97 | throw new RedactionAbortedException("-redact-comments could not redact " + file | |
| 98 | + " (" + languageOf(file) + "): " + e.getMessage(), e); | |
| 99 | } catch (RuntimeException | StackOverflowError e) { | |
| 100 | throw new RedactionAbortedException("-redact-comments redactor '" + redactor.pluginId() | |
| 101 | + "' failed on " + file + " (" + languageOf(file) + "): " + e, e); | |
| 102 | } | |
| 103 | String violation = contractViolation(source, redacted); | |
| 104 |
2
1. redactWith : removed conditional - replaced equality check with false → KILLED 2. redactWith : removed conditional - replaced equality check with true → KILLED |
if (violation != null) { |
| 105 | throw new RedactionAbortedException("-redact-comments redactor '" + redactor.pluginId() | |
| 106 | + "' violated the redaction contract on " + file + " (" + languageOf(file) + "): " | |
| 107 | + violation + "; refusing to send the text to the AI provider."); | |
| 108 | } | |
| 109 |
1
1. redactWith : replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::redactWith → KILLED |
return redacted; |
| 110 | } | |
| 111 | ||
| 112 | /** | |
| 113 | * Checks the post-conditions of the redaction contract. | |
| 114 | * | |
| 115 | * @return a description of the first violation, or {@code null} when the output is valid: | |
| 116 | * non-null, same length, line terminators untouched, and differing from the input | |
| 117 | * only where a non-terminator character became a space | |
| 118 | */ | |
| 119 | private static String contractViolation(String source, String redacted) { | |
| 120 |
2
1. contractViolation : removed conditional - replaced equality check with false → KILLED 2. contractViolation : removed conditional - replaced equality check with true → KILLED |
if (redacted == null) { |
| 121 |
1
1. contractViolation : replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → SURVIVED |
return "returned null"; |
| 122 | } | |
| 123 |
2
1. contractViolation : removed conditional - replaced equality check with false → KILLED 2. contractViolation : removed conditional - replaced equality check with true → KILLED |
if (redacted.length() != source.length()) { |
| 124 |
1
1. contractViolation : replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → SURVIVED |
return "changed the length from " + source.length() + " to " + redacted.length(); |
| 125 | } | |
| 126 |
3
1. contractViolation : changed conditional boundary → KILLED 2. contractViolation : removed conditional - replaced comparison check with true → KILLED 3. contractViolation : removed conditional - replaced comparison check with false → KILLED |
for (int i = 0; i < source.length(); i++) { |
| 127 | char in = source.charAt(i); | |
| 128 | char out = redacted.charAt(i); | |
| 129 |
2
1. contractViolation : removed conditional - replaced equality check with true → KILLED 2. contractViolation : removed conditional - replaced equality check with false → KILLED |
if (in == out) { |
| 130 | continue; | |
| 131 | } | |
| 132 |
4
1. contractViolation : removed conditional - replaced equality check with false → SURVIVED 2. contractViolation : removed conditional - replaced equality check with true → KILLED 3. contractViolation : removed conditional - replaced equality check with false → KILLED 4. contractViolation : removed conditional - replaced equality check with true → KILLED |
if (in == LF || in == CR) { |
| 133 |
1
1. contractViolation : replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → SURVIVED |
return "altered a line terminator at offset " + i; |
| 134 | } | |
| 135 |
2
1. contractViolation : removed conditional - replaced equality check with false → KILLED 2. contractViolation : removed conditional - replaced equality check with true → KILLED |
if (out != BLANK) { |
| 136 |
1
1. contractViolation : replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → SURVIVED |
return "replaced a character with something other than a space at offset " + i; |
| 137 | } | |
| 138 | } | |
| 139 |
1
1. contractViolation : replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → KILLED |
return null; |
| 140 | } | |
| 141 | ||
| 142 | private static String languageOf(Path file) { | |
| 143 | Path name = file.getFileName(); | |
| 144 |
2
1. languageOf : removed conditional - replaced equality check with false → SURVIVED 2. languageOf : removed conditional - replaced equality check with true → SURVIVED |
String text = name == null ? file.toString() : name.toString(); |
| 145 | int dot = text.lastIndexOf('.'); | |
| 146 |
8
1. languageOf : changed conditional boundary → SURVIVED 2. languageOf : Replaced integer subtraction with addition → SURVIVED 3. languageOf : removed conditional - replaced comparison check with true → SURVIVED 4. languageOf : changed conditional boundary → SURVIVED 5. languageOf : removed conditional - replaced comparison check with false → KILLED 6. languageOf : removed conditional - replaced comparison check with false → KILLED 7. languageOf : removed conditional - replaced comparison check with true → KILLED 8. languageOf : replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::languageOf → KILLED |
return dot >= 0 && dot < text.length() - 1 ? "*" + text.substring(dot) : text; |
| 147 | } | |
| 148 | } | |
Mutations | ||
| 54 |
1.1 |
|
| 69 |
1.1 2.2 |
|
| 74 |
1.1 2.2 |
|
| 75 |
1.1 |
|
| 104 |
1.1 2.2 |
|
| 109 |
1.1 |
|
| 120 |
1.1 2.2 |
|
| 121 |
1.1 |
|
| 123 |
1.1 2.2 |
|
| 124 |
1.1 |
|
| 126 |
1.1 2.2 3.3 |
|
| 129 |
1.1 2.2 |
|
| 132 |
1.1 2.2 3.3 4.4 |
|
| 133 |
1.1 |
|
| 135 |
1.1 2.2 |
|
| 136 |
1.1 |
|
| 139 |
1.1 |
|
| 144 |
1.1 2.2 |
|
| 146 |
1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 |