|
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.charset.StandardCharsets; |
|
7
|
|
import java.nio.file.Path; |
|
8
|
|
import java.nio.file.Paths; |
|
9
|
|
import java.security.MessageDigest; |
|
10
|
|
import java.security.NoSuchAlgorithmException; |
|
11
|
|
import java.util.HexFormat; |
|
12
|
|
import java.util.List; |
|
13
|
|
|
|
14
|
|
/** |
|
15
|
|
* Pure utility helpers for content fingerprints and scan-root path prefixes. |
|
16
|
|
* |
|
17
|
|
* <p> |
|
18
|
|
* Two unrelated-but-small concerns live here because both are stateless, |
|
19
|
|
* dependency-free, and named-focused. Pulling them into one |
|
20
|
|
* single-responsibility utility class keeps each concern visible without |
|
21
|
|
* the cost of constructor injection — both methods are pure functions |
|
22
|
|
* mandated by their specifications (SHA-256 for hashing, OS-native |
|
23
|
|
* relativisation for the prefix), so there is nothing to substitute: |
|
24
|
|
* </p> |
|
25
|
|
* <ul> |
|
26
|
|
* <li>{@link #hashClass(String)} — computes a SHA-256 fingerprint of the |
|
27
|
|
* canonical pretty-printed AST text of a class. This is the value |
|
28
|
|
* exposed as {@code content_hash} in CSV / SARIF output and used as the |
|
29
|
|
* cache key by {@link org.egothor.methodatlas.AiResultCache}.</li> |
|
30
|
|
* <li>{@link #filePrefix(List)} — derives the forward-slashed path prefix |
|
31
|
|
* used in GitHub Actions workflow annotations and SARIF location URIs, |
|
32
|
|
* relativised to the current working directory so that paths resolve |
|
33
|
|
* to inline positions in PR diffs.</li> |
|
34
|
|
* </ul> |
|
35
|
|
* |
|
36
|
|
* <p> |
|
37
|
|
* Both methods are pure functions and therefore exposed as {@code static}. |
|
38
|
|
* Test code calls them directly with handcrafted inputs; no dependency |
|
39
|
|
* injection is needed because there is nothing to substitute — the SHA-256 |
|
40
|
|
* implementation is mandated by the Java SE specification and the path |
|
41
|
|
* relativisation has only one correct answer. |
|
42
|
|
* </p> |
|
43
|
|
* |
|
44
|
|
* @see ScanOrchestrator |
|
45
|
|
* @since 1.0.0 |
|
46
|
|
*/ |
|
47
|
|
public final class ContentHasher { |
|
48
|
|
|
|
49
|
|
private ContentHasher() { |
|
50
|
|
// Utility class; instantiation is prevented to make the static |
|
51
|
|
// intent obvious to callers and to satisfy PMD. |
|
52
|
|
} |
|
53
|
|
|
|
54
|
|
/** |
|
55
|
|
* Computes a SHA-256 content fingerprint of a class source string. |
|
56
|
|
* |
|
57
|
|
* <p> |
|
58
|
|
* The input is expected to be the canonical AST text of the class — for |
|
59
|
|
* Java this is the JavaParser pretty-printed form, which normalises |
|
60
|
|
* whitespace and comments so that semantically equivalent classes that |
|
61
|
|
* differ only in formatting produce identical hashes. The output is |
|
62
|
|
* suitable for incremental scanning, AI-cache lookups, and audit |
|
63
|
|
* traceability across two pipeline stages. |
|
64
|
|
* </p> |
|
65
|
|
* |
|
66
|
|
* <p> |
|
67
|
|
* Algorithm: SHA-256 (FIPS 180-4) applied to the UTF-8 bytes of |
|
68
|
|
* {@code classSource}. Time complexity is {@code O(n)} in the source |
|
69
|
|
* size. The result is a 64-character lowercase hexadecimal string. |
|
70
|
|
* </p> |
|
71
|
|
* |
|
72
|
|
* @param classSource canonical pretty-printed form of the class |
|
73
|
|
* declaration; must not be {@code null} |
|
74
|
|
* @return 64-character lowercase hexadecimal SHA-256 digest; never |
|
75
|
|
* {@code null}, never empty |
|
76
|
|
* @throws IllegalStateException if SHA-256 is unavailable — never in |
|
77
|
|
* practice, because SHA-256 is mandated by |
|
78
|
|
* the Java SE specification |
|
79
|
|
*/ |
|
80
|
|
public static String hashClass(String classSource) { |
|
81
|
|
try { |
|
82
|
|
MessageDigest digest = MessageDigest.getInstance("SHA-256"); |
|
83
|
|
byte[] bytes = digest.digest(classSource.getBytes(StandardCharsets.UTF_8)); |
|
84
|
1
1. hashClass : replaced return value with "" for org/egothor/methodatlas/command/ContentHasher::hashClass → KILLED
|
return HexFormat.of().formatHex(bytes); |
|
85
|
|
} catch (NoSuchAlgorithmException e) { |
|
86
|
|
throw new IllegalStateException("SHA-256 not available", e); |
|
87
|
|
} |
|
88
|
|
} |
|
89
|
|
|
|
90
|
|
/** |
|
91
|
|
* Derives the forward-slashed path prefix used in GitHub Actions |
|
92
|
|
* workflow annotations and SARIF location URIs. |
|
93
|
|
* |
|
94
|
|
* <p> |
|
95
|
|
* The first configured scan root is relativised against the current |
|
96
|
|
* working directory and converted to forward slashes. A trailing slash |
|
97
|
|
* is appended unless the prefix is empty. The resulting string is |
|
98
|
|
* concatenated with the per-method relative path to produce annotation |
|
99
|
|
* paths that GitHub resolves to inline positions in the PR diff |
|
100
|
|
* (for example {@code src/test/java/com/acme/AuthTest.java}). |
|
101
|
|
* </p> |
|
102
|
|
* |
|
103
|
|
* <p> |
|
104
|
|
* When {@code roots} is empty the returned prefix is the empty string, |
|
105
|
|
* which produces unprefixed annotation paths — appropriate when no scan |
|
106
|
|
* root was configured because the caller is operating on the current |
|
107
|
|
* directory directly. |
|
108
|
|
* </p> |
|
109
|
|
* |
|
110
|
|
* <p> |
|
111
|
|
* On Windows, scan roots that resolve to a different drive than the |
|
112
|
|
* current working directory cannot be relativised. The method falls |
|
113
|
|
* back to the absolute path of the root in that case rather than |
|
114
|
|
* throwing. |
|
115
|
|
* </p> |
|
116
|
|
* |
|
117
|
|
* @param roots configured scan roots; must not be {@code null}; may be |
|
118
|
|
* empty |
|
119
|
|
* @return forward-slash path ending with {@code /}, or the empty string |
|
120
|
|
* when {@code roots} is empty |
|
121
|
|
*/ |
|
122
|
|
public static String filePrefix(List<Path> roots) { |
|
123
|
2
1. filePrefix : removed conditional - replaced equality check with false → KILLED
2. filePrefix : removed conditional - replaced equality check with true → KILLED
|
if (roots.isEmpty()) { |
|
124
|
|
return ""; |
|
125
|
|
} |
|
126
|
|
Path root = roots.get(0).toAbsolutePath().normalize(); |
|
127
|
|
String prefix; |
|
128
|
|
try { |
|
129
|
|
Path cwd = Paths.get("").toAbsolutePath(); |
|
130
|
|
prefix = cwd.relativize(root).toString().replace('\\', '/'); |
|
131
|
|
} catch (IllegalArgumentException e) { |
|
132
|
|
// Different drive on Windows — fall back to the absolute path. |
|
133
|
|
prefix = root.toString().replace('\\', '/'); |
|
134
|
|
} |
|
135
|
4
1. filePrefix : removed conditional - replaced equality check with true → SURVIVED
2. filePrefix : removed conditional - replaced equality check with true → SURVIVED
3. filePrefix : removed conditional - replaced equality check with false → KILLED
4. filePrefix : removed conditional - replaced equality check with false → KILLED
|
if (!prefix.isEmpty() && !prefix.endsWith("/")) { |
|
136
|
|
prefix += "/"; |
|
137
|
|
} |
|
138
|
1
1. filePrefix : replaced return value with "" for org/egothor/methodatlas/command/ContentHasher::filePrefix → KILLED
|
return prefix; |
|
139
|
|
} |
|
140
|
|
} |
| | Mutations |
| 84 |
|
1.1 Location : hashClass Killed by : org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:hashClass_returnsLowercaseHex64Characters()] replaced return value with "" for org/egothor/methodatlas/command/ContentHasher::hashClass → KILLED
|
| 123 |
|
1.1 Location : filePrefix Killed by : org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:filePrefix_emptyList_returnsEmptyString()] removed conditional - replaced equality check with false → KILLED
2.2 Location : filePrefix Killed by : org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:filePrefix_relativeRoot_endsWithSlash(java.nio.file.Path)] removed conditional - replaced equality check with true → KILLED
|
| 135 |
|
1.1 Location : filePrefix Killed by : org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:filePrefix_relativeRoot_endsWithSlash(java.nio.file.Path)] removed conditional - replaced equality check with false → KILLED
2.2 Location : filePrefix Killed by : org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:filePrefix_relativeRoot_endsWithSlash(java.nio.file.Path)] removed conditional - replaced equality check with false → KILLED
3.3 Location : filePrefix Killed by : none removed conditional - replaced equality check with true → SURVIVED
Covering tests
Covered by tests:
- org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:filePrefix_usesForwardSlashesOnAllPlatforms(java.nio.file.Path)]
- org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:filePrefix_relativeRoot_endsWithSlash(java.nio.file.Path)]
- org.egothor.methodatlas.GitHubAnnotationsEmitterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.GitHubAnnotationsEmitterTest]/[method:app_githubAnnotationsMode_emptyDirectoryProducesNoOutput(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_toolDriverNameIsMethodAtlas(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_outputIsNotCsvHeader(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_emitsValidSarifDocument(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:configFile_overridesOutputMode_toSarif(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:cliFlag_overridesConfigFileOutputMode(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_allMethodsUseLevelNone_whenAiDisabled(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_resultHasPhysicalAndLogicalLocation(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_emitsOneResultPerTestMethod(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppContentHashTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppContentHashTest]/[method:sarifMode_contentHashPresentAndValid_whenEnabled(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppContentHashTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppContentHashTest]/[method:sarifMode_contentHashAbsent_byDefault(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppContentHashTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppContentHashTest]/[method:sarifMode_methodsInSameClass_shareIdenticalHashInSarif(java.nio.file.Path)]
- org.egothor.methodatlas.GitHubAnnotationsEmitterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.GitHubAnnotationsEmitterTest]/[method:app_githubAnnotationsMode_emitsAnnotationsForSecurityMethods(java.nio.file.Path)]
4.4 Location : filePrefix Killed by : none removed conditional - replaced equality check with true → SURVIVED
Covering tests
Covered by tests:
- org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:filePrefix_usesForwardSlashesOnAllPlatforms(java.nio.file.Path)]
- org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:filePrefix_relativeRoot_endsWithSlash(java.nio.file.Path)]
- org.egothor.methodatlas.GitHubAnnotationsEmitterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.GitHubAnnotationsEmitterTest]/[method:app_githubAnnotationsMode_emptyDirectoryProducesNoOutput(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_toolDriverNameIsMethodAtlas(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_outputIsNotCsvHeader(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_emitsValidSarifDocument(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:configFile_overridesOutputMode_toSarif(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:cliFlag_overridesConfigFileOutputMode(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_allMethodsUseLevelNone_whenAiDisabled(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_resultHasPhysicalAndLogicalLocation(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_emitsOneResultPerTestMethod(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppContentHashTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppContentHashTest]/[method:sarifMode_contentHashPresentAndValid_whenEnabled(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppContentHashTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppContentHashTest]/[method:sarifMode_contentHashAbsent_byDefault(java.nio.file.Path)]
- org.egothor.methodatlas.MethodAtlasAppContentHashTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppContentHashTest]/[method:sarifMode_methodsInSameClass_shareIdenticalHashInSarif(java.nio.file.Path)]
- org.egothor.methodatlas.GitHubAnnotationsEmitterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.GitHubAnnotationsEmitterTest]/[method:app_githubAnnotationsMode_emitsAnnotationsForSecurityMethods(java.nio.file.Path)]
|
| 138 |
|
1.1 Location : filePrefix Killed by : org.egothor.methodatlas.command.ContentHasherTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.command.ContentHasherTest]/[method:filePrefix_relativeRoot_endsWithSlash(java.nio.file.Path)] replaced return value with "" for org/egothor/methodatlas/command/ContentHasher::filePrefix → KILLED
|