CommentRedaction.java

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
Location : none
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:fileWithoutExtension_isReportedByName()]
replaced return value with null for org/egothor/methodatlas/command/CommentRedaction::none → KILLED

69

1.1
Location : redactOrThrow
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:fileWithoutExtension_isReportedByName()]
removed conditional - replaced equality check with true → KILLED

2.2
Location : redactOrThrow
Killed by : org.egothor.methodatlas.command.ScanOrchestratorRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ScanOrchestratorRedactionTest]/[method:missingFilePath_abortsFailClosed()]
removed conditional - replaced equality check with false → KILLED

74

1.1
Location : redactOrThrow
Killed by : org.egothor.methodatlas.command.ScanOrchestratorRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ScanOrchestratorRedactionTest]/[method:requireRedactable_rejectsUnsupportedFileUpFront()]
removed conditional - replaced equality check with true → KILLED

2.2
Location : redactOrThrow
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
removed conditional - replaced equality check with false → KILLED

75

1.1
Location : redactOrThrow
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::redactOrThrow → KILLED

104

1.1
Location : redactWith
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:contractViolations_failClosed()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : redactWith
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
removed conditional - replaced equality check with true → KILLED

109

1.1
Location : redactWith
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::redactWith → KILLED

120

1.1
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:contractViolations_failClosed()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
removed conditional - replaced equality check with true → KILLED

121

1.1
Location : contractViolation
Killed by : none
replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → SURVIVED
Covering tests

123

1.1
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:contractViolations_failClosed()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
removed conditional - replaced equality check with true → KILLED

124

1.1
Location : contractViolation
Killed by : none
replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → SURVIVED
Covering tests

126

1.1
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
changed conditional boundary → KILLED

2.2
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
removed conditional - replaced comparison check with true → KILLED

3.3
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:contractViolations_failClosed()]
removed conditional - replaced comparison check with false → KILLED

129

1.1
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:contractViolations_failClosed()]
removed conditional - replaced equality check with true → KILLED

2.2
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
removed conditional - replaced equality check with false → KILLED

132

1.1
Location : contractViolation
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

2.2
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
removed conditional - replaced equality check with true → KILLED

3.3
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
removed conditional - replaced equality check with false → KILLED

4.4
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:contractViolations_failClosed()]
removed conditional - replaced equality check with true → KILLED

133

1.1
Location : contractViolation
Killed by : none
replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → SURVIVED
Covering tests

135

1.1
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:contractViolations_failClosed()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
removed conditional - replaced equality check with true → KILLED

136

1.1
Location : contractViolation
Killed by : none
replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → SURVIVED
Covering tests

139

1.1
Location : contractViolation
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:firstSupportingRedactorWins()]
replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::contractViolation → KILLED

144

1.1
Location : languageOf
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

2.2
Location : languageOf
Killed by : none
removed conditional - replaced equality check with true → SURVIVED Covering tests

146

1.1
Location : languageOf
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:unsupportedFile_namesFileAndLanguage()]
removed conditional - replaced comparison check with false → KILLED

2.2
Location : languageOf
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:unsupportedFile_namesFileAndLanguage()]
removed conditional - replaced comparison check with false → KILLED

3.3
Location : languageOf
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

4.4
Location : languageOf
Killed by : none
Replaced integer subtraction with addition → SURVIVED Covering tests

5.5
Location : languageOf
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:fileWithoutExtension_isReportedByName()]
removed conditional - replaced comparison check with true → KILLED

6.6
Location : languageOf
Killed by : org.egothor.methodatlas.command.CommentRedactionTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.CommentRedactionTest]/[method:fileWithoutExtension_isReportedByName()]
replaced return value with "" for org/egothor/methodatlas/command/CommentRedaction::languageOf → KILLED

7.7
Location : languageOf
Killed by : none
removed conditional - replaced comparison check with true → SURVIVED Covering tests

8.8
Location : languageOf
Killed by : none
changed conditional boundary → SURVIVED Covering tests

Active mutators

Tests examined


Report generated by PIT 1.30.1 support