Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions eo-maven-plugin/src/main/java/org/eolang/maven/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Comparator;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.cactoos.Func;
import org.cactoos.func.UncheckedFunc;
Expand All @@ -34,6 +35,11 @@ final class Cache {
*/
private final Func<Path, String> compilation;

/**
* Files filter for dir cache.
*/
private final Predicate<Path> filter;

/**
* Constructor.
* @param path Cache path
Expand All @@ -49,8 +55,23 @@ final class Cache {
* @param compilation Compilation function
*/
Cache(final Path base, final Func<Path, String> compilation) {
this(base, compilation, p -> true);
}

/**
* Constructor.
* @param base Base cache directory.
* @param compilation Compilation function.
* @param filter Filter for files.
*/
Cache(
final Path base,
final Func<Path, String> compilation,
final Predicate<Path> filter
) {
this.base = base;
this.compilation = compilation;
this.filter = filter;
}

/**
Expand All @@ -61,7 +82,7 @@ final class Cache {
*/
public void apply(final Path source, final Path target, final Path tail) {
try {
final String sha = Cache.sha(source);
final String sha = this.sha(source);
final Path hash = this.hash(tail);
final Path cache = this.base.resolve(tail);
if (
Expand Down Expand Up @@ -99,10 +120,10 @@ private Path hash(final Path tail) {
* @param any File or directory path
* @return Base64-encoded SHA-256 hash of the file or directory contents
*/
private static String sha(final Path any) {
private String sha(final Path any) {
final String result;
if (Files.isDirectory(any)) {
result = Cache.dirSha(any);
result = this.dirSha(any);
} else if (Files.isRegularFile(any)) {
result = Cache.fileSha(any);
} else {
Expand All @@ -118,11 +139,12 @@ private static String sha(final Path any) {
* @param dir Directory path.
* @return Base64-encoded SHA-256 hash of the directory contents.
*/
private static String dirSha(final Path dir) {
private String dirSha(final Path dir) {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (Stream<Path> stream = Files.walk(dir)) {
stream.filter(Files::isRegularFile)
.filter(this.filter::test)
.sorted(Comparator.comparing(Path::toString))
Comment on lines 67 to 148

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new Cache feature was added (directory hashing filter via Predicate<Path>), but there’s no unit test coverage to confirm that excluded files don’t affect the computed hash and that included files do. Since this is now relied on by WPA caching, a focused test would help prevent regressions (e.g., create a directory with two files, exclude one via filter, modify it, and assert the cache does not recompile).

Copilot uses AI. Check for mistakes.
.map(Cache::fileSha)
.map(s -> s.getBytes(StandardCharsets.UTF_8))
Expand Down
55 changes: 53 additions & 2 deletions eo-maven-plugin/src/main/java/org/eolang/maven/MjLint.java
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,40 @@ private int lintAll(final Map<Severity, Integer> counts) throws IOException {
if (!this.skipProgramLints.isEmpty()) {
Logger.info(this, "Unliting WPA lints: %[list]s", this.skipProgramLints);
}
final List<Defect> defects;
if (this.cacheEnabled) {
final Path wpa = Path.of("wpa.xmir");
final Path target = this.targetDir.toPath().resolve(MjLint.DIR).resolve(wpa);
new Cache(
this.cache.toPath().resolve(MjLint.CACHE),
root -> {
Logger.info(this, "Linting a package");
final Directives all = new Directives().add("defects");
for (final Defect defect : this.wpa(pkg)) {
MjLint.embedded(all, defect);
}
all.up();
return new Xembler(all).xmlQuietly();
},
p -> p.getFileName().toString().endsWith(".xmir")
&& !p.getFileName().equals(wpa)
).apply(this.sourcesDir.toPath(), target, wpa);
Comment on lines +212 to +227

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the WPA cached branch, the cache key is computed from this.sourcesDir but the directory hash is filtered to only include .xmir files (excluding wpa.xmir). sourcesDir defaults to src/main/eo, so the hash will likely be computed from an empty file set (or at least ignore the real WPA inputs), causing stale cache reuse across source changes and potential cross-project cache collisions.

Consider hashing the actual WPA inputs (e.g., the XMIR directory under targetDir, or the EO sources with a .eo filter), and/or using a CachePath that includes plugin version + a project/package-specific component so wpa.xmir doesn’t live at a global fixed location.

Copilot uses AI. Check for mistakes.
defects = MjLint.read(target);
} else {
Logger.info(
this,
"Linting a package without cache, this might be slow, consider enabling cache"
);
defects = this.wpa(pkg);
}
for (final Defect defect : defects) {
counts.compute(defect.severity(), (sev, before) -> before + 1);
}
Comment on lines +210 to +238

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With cache enabled, WPA defects are only logged during a cache miss (inside the compilation lambda via this.wpa(pkg)). On a cache hit you read cached defects and update counts, but nothing is logged, which makes output depend on cache state (unlike lintOne, which logs every run even when cached).

If consistent logging is desired, log defects after loading them from cache as well. That likely requires caching/restoring the full defect details (object/line/text) instead of dropping them in read().

Copilot uses AI. Check for mistakes.
return pkg.size();
}

private List<Defect> wpa(final Map<String, XML> pkg) {
final List<Defect> defects = new ArrayList<>(0);
new Program(pkg)
.without(this.skipProgramLints.toArray(new String[0]))
.defects()
Expand All @@ -222,12 +256,12 @@ private int lintAll(final Map<Severity, Integer> counts) throws IOException {
)
).applyQuietly(node);
if (MjLint.notSuppressed(new Xnav(node), defect)) {
counts.compute(defect.severity(), (sev, before) -> before + 1);
defects.add(defect);
MjLint.logOne(defect);
}
}
);
return pkg.size();
return defects;
}

/**
Expand Down Expand Up @@ -408,6 +442,23 @@ private static Directives embedded(final Directives dirs, final Defect defect) {
return dirs.up();
}

/**
* Read defects from XMIR.
* @param path Path to XMIR
* @return Collection of defects
*/
private static List<Defect> read(final Path path) {
return new Xnav(path).path("/defects/error").map(
node -> new Defect.Default(
node.attribute("check").text().orElseThrow(),
Severity.parsed(node.attribute("severity").text().orElseThrow()),
"",
0,
""
)
).collect(Collectors.toList());
Comment on lines +450 to +459

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read() uses orElseThrow() for required attributes, which will throw a generic NoSuchElementException if the cache XMIR is corrupted/partial. Elsewhere in this class (see existing()), missing required attributes throw an exception with a clear message.

It would be more diagnosable to throw with an explicit message here too (e.g., "Cached WPA defect must contain 'check'/'severity' attribute").

Copilot uses AI. Check for mistakes.
}
Comment on lines +445 to +460

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Defect data is discarded when reading from cache.

The read method only extracts check and severity, but ignores line attribute and text content that are actually persisted by embedded(). While this works for the current counting-only usage, it loses defect fidelity.

Consider extracting available data to preserve full defect information:

Proposed fix to preserve defect data
     private static List<Defect> read(final Path path) {
         return new Xnav(path).path("/defects/error").map(
             node -> new Defect.Default(
                 node.attribute("check").text().orElseThrow(),
                 Severity.parsed(node.attribute("severity").text().orElseThrow()),
                 "",
-                0,
-                ""
+                Integer.parseInt(node.attribute("line").text().orElse("0")),
+                node.text().orElse("")
             )
         ).collect(Collectors.toList());
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eo-maven-plugin/src/main/java/org/eolang/maven/MjLint.java` around lines 445
- 460, The read method currently discards defect details (only using check and
severity); update it to extract and preserve the defect's optional attributes
and content from the Xnav node: read
node.attribute("line").text().map(Integer::parseInt).orElse(0) for the line
number, node.text().orElse("") for the defect message, and if present
node.attribute("file").text().orElse("") for the file/path, then pass these
values into the Defect.Default constructor (while keeping Severity.parsed(...)
for severity) so cached XMIR defects retain full fidelity; use safe parsing with
defaults to avoid exceptions in read/Xnav handling.


/**
* This defect is not suppressed?
* @param xnav The XMIR as {@link Xnav}
Expand Down
37 changes: 17 additions & 20 deletions eo-maven-plugin/src/test/java/org/eolang/maven/MjLintTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@
* Test cases for {@link MjLint}.
*
* @since 0.31.0
* @todo #4940:90min Enable MjLintTests when WPA cache is ready.
* We need to enable the following test when we implement WPA cache.
* {@link MjLintTest#savesForWholeProgramAnalysisResultsToCache}
* For now, WPA results are not saved to cache.
*/
@SuppressWarnings({"PMD.AvoidDuplicateLiterals", "PMD.TooManyMethods"})
@ExtendWith(MktmpResolver.class)
Expand Down Expand Up @@ -118,30 +114,31 @@ void detectsWholeProgramAnalysisErrorsOnSecondRun(@Mktmp final Path temp) throws
}

@Test
@Disabled
@SuppressWarnings({
"PMD.UnitTestContainsTooManyAsserts",
"PMD.UnnecessaryLocalRule"
})
void savesForWholeProgramAnalysisResultsToCache(@Mktmp final Path temp) throws IOException {
final Path cache = temp.resolve("wpa-cache");
final String hash = "abcdefq";
final FakeMaven maven = new FakeMaven(temp)
.with("lintAsPackage", true)
.allTojosWithHash(() -> hash)
.allTojosWithHash(() -> "abcdefq")
.with("cache", cache.toFile())
.withProgram(MjLintTest.probmlematic());
Assertions.assertThrows(
IllegalStateException.class,
() -> maven.execute(new FakeMaven.Lint()),
"We should get WPA error, but we got it"
);
.withProgram(
"+home https://www.eolang.org",
"+package foo.x",
"+version 0.0.0",
"+unlint empty-object",
"+unlint unit-test-missing",
"+unlint mandatory-spdx",
"+unlint comment-too-short",
"+unlint object-has-data",
"",
"# No comments.",
"[x] > main",
" (stdout \"Hello!\" x).print > @"
);
maven.execute(new FakeMaven.Lint());
MatcherAssert.assertThat(
"WPA results must be saved to cache",
cache.resolve(MjLint.CACHE)
.resolve(FakeMaven.pluginVersion())
.resolve(hash)
.resolve("foo/x/wpa.xmir").toFile(),
.resolve("wpa.xmir").toFile(),
FileMatchers.anExistingFile()
);
}
Expand Down
Loading