S8 integrate pinned Palantir formatting through OpenRewrite
This commit is contained in:
55
README.md
55
README.md
@@ -253,3 +253,58 @@ Only successful `main` runs publish images to the Synology registry. See
|
|||||||
[the CI management guide](docs/gitea-ci.md) for runner setup, registry secrets,
|
[the CI management guide](docs/gitea-ci.md) for runner setup, registry secrets,
|
||||||
image names, diagnostics and recovery, and [validation evidence](docs/s4-validation.md)
|
image names, diagnostics and recovery, and [validation evidence](docs/s4-validation.md)
|
||||||
for the current operational status.
|
for the current operational status.
|
||||||
|
|
||||||
|
### Java formatting (S8)
|
||||||
|
|
||||||
|
Maven is the authoritative formatting path. The root build runs the actual
|
||||||
|
Palantir Java Format **2.101.0**, Palantir style, through a custom OpenRewrite
|
||||||
|
**8.63.4** text recipe in `tooling/format/Format.java`. The pinned
|
||||||
|
`exec-maven-plugin` **3.6.3** launches that helper with JDK 25. Native import
|
||||||
|
ordering and unused-import removal are enabled; extra Javadoc formatting is off.
|
||||||
|
No OpenRewrite AutoFormat or additional refactoring recipes run.
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mvn -N exec:exec@format # apply, without application compilation
|
||||||
|
mvn -N exec:exec@format -Dformat.mode=check # check, without application compilation
|
||||||
|
mvn compile # applies during validate, before compilation
|
||||||
|
mvn -Dformat.mode=check clean verify # CI: fails before compilation on differences
|
||||||
|
```
|
||||||
|
|
||||||
|
Root `compile`, `test`, `verify` and `install` may rewrite maintained Java files
|
||||||
|
in the validate phase. CI's Java orchestration passes `-Dformat.mode=check` and
|
||||||
|
never repairs source files. On failure, the output lists paths and the apply
|
||||||
|
command. Apply locally, inspect and commit the result, then repeat the check.
|
||||||
|
Run root Maven commands; selecting only the backend with `-pl agiledancers-be`
|
||||||
|
bypasses the root formatting execution.
|
||||||
|
|
||||||
|
The scope is every `.java` file recursively under `agiledancers-be/src/main/java`,
|
||||||
|
`agiledancers-be/src/test/java`, `ci` (including the runner helper), and the
|
||||||
|
formatter helper itself. Generated `target` files and frontend files are excluded.
|
||||||
|
The adapter keeps paths and UTF-8 content, computes changes before writing, and
|
||||||
|
uses atomic per-file replacements. Unsupported syntax fails with its path/cause.
|
||||||
|
The source launcher compiles the adapter directly in memory from a clean checkout;
|
||||||
|
no custom recipe jar needs compiling or installing beforehand. Maven downloads
|
||||||
|
only pinned external tooling dependencies on first use.
|
||||||
|
|
||||||
|
Successfully checked/formatted content hashes are cached in
|
||||||
|
`target/palantir-cache.properties`; new or changed files are always processed.
|
||||||
|
The root POM, adapter source, dependency classpath and JDK version invalidate the cache. `clean` removes
|
||||||
|
it. Check and apply share the engine, options, scope and successful-content cache;
|
||||||
|
a failing check does not write sources or update the cache. For controlled timing
|
||||||
|
comparisons only, `-Dformat.skip=true` disables the execution.
|
||||||
|
|
||||||
|
Editors are optional. Palantir supplies an IntelliJ plugin (see the
|
||||||
|
[upstream instructions](https://github.com/palantir/palantir-java-format#intellij-plugin)).
|
||||||
|
Use it only with the same engine version/style and Javadoc setting; if the editor
|
||||||
|
cannot match them, use Maven to format instead. Editor import optimization or
|
||||||
|
format-on-save must not override Maven's result. Other editors can invoke the
|
||||||
|
standalone Maven apply command as an external tool.
|
||||||
|
|
||||||
|
Verification and measurements are recorded in
|
||||||
|
[the S8 evidence](specs/S8-formatting-evidence.md). Reproduce the isolated failure,
|
||||||
|
UTF-8, import, cache and idempotence checks with
|
||||||
|
`python3 tooling/format/test.py`, and the sequential end-to-end build measurements
|
||||||
|
with `python3 tooling/format/benchmark.py` (requires the ordinary backend test
|
||||||
|
prerequisites). Results are written under `target/`.
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ class CiTests {
|
|||||||
f.fake.mavenFailure = true;
|
f.fake.mavenFailure = true;
|
||||||
try { f.pipeline.verify(); throw new AssertionError("Expected failure"); }
|
try { f.pipeline.verify(); throw new AssertionError("Expected failure"); }
|
||||||
catch (Commands.Failure error) { equal(42,error.exitCode); }
|
catch (Commands.Failure error) { equal(42,error.exitCode); }
|
||||||
|
equal(List.of("mvn", "-B", "-ntp", "-Dformat.mode=check", "clean", "verify"),
|
||||||
|
f.fake.calls.stream().filter(a -> a.getFirst().equals("mvn")).findFirst().orElseThrow());
|
||||||
equal(false,Files.exists(f.root.resolve("ci/artifacts/verified-commit")));
|
equal(false,Files.exists(f.root.resolve("ci/artifacts/verified-commit")));
|
||||||
equal(0L,f.calls("build")); equal(0L,f.calls("push"));
|
equal(0L,f.calls("build")); equal(0L,f.calls("push"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class Pipeline {
|
|||||||
try {
|
try {
|
||||||
requirePlatform(); isolateDocker();
|
requirePlatform(); isolateDocker();
|
||||||
check(commit.equals(capture("git", "rev-parse", "HEAD")), "Checkout does not match pushed commit");
|
check(commit.equals(capture("git", "rev-parse", "HEAD")), "Checkout does not match pushed commit");
|
||||||
run("java", "-version"); run("node", "--version"); run("mvn", "-B", "-ntp", "clean", "verify");
|
run("java", "-version"); run("node", "--version"); run("mvn", "-B", "-ntp", "-Dformat.mode=check", "clean", "verify");
|
||||||
CiTests.run();
|
CiTests.run();
|
||||||
Path frontend = root.resolve("agiledancers-fe");
|
Path frontend = root.resolve("agiledancers-fe");
|
||||||
verifyFrontend();
|
verifyFrontend();
|
||||||
|
|||||||
49
pom.xml
49
pom.xml
@@ -24,6 +24,11 @@
|
|||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
<quarkus.platform.version>3.40.1</quarkus.platform.version>
|
<quarkus.platform.version>3.40.1</quarkus.platform.version>
|
||||||
|
|
||||||
|
<format.mode>apply</format.mode>
|
||||||
|
<format.skip>false</format.skip>
|
||||||
|
<palantir-java-format.version>2.101.0</palantir-java-format.version>
|
||||||
|
<rewrite-core.version>8.63.4</rewrite-core.version>
|
||||||
|
|
||||||
<ortools-java.version>9.15.6755</ortools-java.version>
|
<ortools-java.version>9.15.6755</ortools-java.version>
|
||||||
<lombok.version>1.18.48</lombok.version>
|
<lombok.version>1.18.48</lombok.version>
|
||||||
|
|
||||||
@@ -77,6 +82,50 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<version>3.6.3</version>
|
||||||
|
<inherited>false</inherited>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>format</id>
|
||||||
|
<phase>validate</phase>
|
||||||
|
<goals><goal>exec</goal></goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<skip>${format.skip}</skip>
|
||||||
|
<executable>${java.home}/bin/java</executable>
|
||||||
|
<includeProjectDependencies>false</includeProjectDependencies>
|
||||||
|
<includePluginDependencies>true</includePluginDependencies>
|
||||||
|
<arguments>
|
||||||
|
<argument>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</argument>
|
||||||
|
<argument>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</argument>
|
||||||
|
<argument>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</argument>
|
||||||
|
<argument>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</argument>
|
||||||
|
<argument>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</argument>
|
||||||
|
<argument>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</argument>
|
||||||
|
<argument>--class-path</argument><classpath/>
|
||||||
|
<argument>${maven.multiModuleProjectDirectory}/tooling/format/Format.java</argument>
|
||||||
|
<argument>${maven.multiModuleProjectDirectory}</argument>
|
||||||
|
<argument>${format.mode}</argument>
|
||||||
|
</arguments>
|
||||||
|
</configuration>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.palantir.javaformat</groupId>
|
||||||
|
<artifactId>palantir-java-format</artifactId>
|
||||||
|
<version>${palantir-java-format.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openrewrite</groupId>
|
||||||
|
<artifactId>rewrite-core</artifactId>
|
||||||
|
<version>${rewrite-core.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
|||||||
96
specs/S8-formatting-evidence.md
Normal file
96
specs/S8-formatting-evidence.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# S8 implementation evidence
|
||||||
|
|
||||||
|
Measured on 2026-10-30 on Linux x86_64 (kernel 7.0.0-38-generic), AMD Ryzen 7
|
||||||
|
7730U, 16 logical CPUs; Ubuntu OpenJDK 25.0.4.1, Maven 3.9.16. These are local
|
||||||
|
wall-clock measurements, not universal performance guarantees.
|
||||||
|
|
||||||
|
## Integration and scope
|
||||||
|
|
||||||
|
- exec-maven-plugin 3.6.3; OpenRewrite core 8.63.4; Palantir Java Format 2.101.0.
|
||||||
|
- Custom `Format.PalantirRecipe` processes OpenRewrite PlainText source files
|
||||||
|
using Palantir's `formatSourceAndFixImports`, Palantir style, `formatJavadoc(false)`.
|
||||||
|
No Java type attribution, OpenRewrite Java parser or AutoFormat is involved.
|
||||||
|
- 73 maintained Java files: 65 backend production/test files, 7 CI/runner helpers,
|
||||||
|
and the formatter helper. Recursive discovery excludes generated build output.
|
||||||
|
- Root validate executes before compilation; compile/test/verify/install all reach
|
||||||
|
validate. CI explicitly passes check mode to clean verify. Standalone commands
|
||||||
|
run only the root exec execution and do not compile the backend.
|
||||||
|
- Bootstrap was first executed with neither Palantir nor OpenRewrite in the local
|
||||||
|
Maven repository. The helper is source-launched, with dependencies supplied by
|
||||||
|
Maven's plugin classpath. There is no project recipe artifact to preinstall.
|
||||||
|
The isolated regression fixture also has no compiled adapter or backend module.
|
||||||
|
|
||||||
|
## Correctness
|
||||||
|
|
||||||
|
- Full `mvn -B -ntp clean verify` passed after formatting: 33 backend tests,
|
||||||
|
zero failures/errors/skips. Both benchmark clean verifications also pass.
|
||||||
|
- `java ci/Ci.java test`: 17 passed, including assertion of the exact check-only
|
||||||
|
Maven invocation and that Maven failure removes the success marker and prevents
|
||||||
|
image build/publication.
|
||||||
|
- `python3 tooling/format/test.py` checks an isolated source-launcher checkout:
|
||||||
|
read-only check failure with path and apply command; native unused-import
|
||||||
|
removal; UTF-8 non-ASCII preservation; check/apply agreement; second uncached
|
||||||
|
check with no differences; malformed Java fails with its path and leaves the
|
||||||
|
source bytes unchanged; a newly introduced file cannot evade the content cache.
|
||||||
|
- All current Java files have been processed by the real engine on JDK 25,
|
||||||
|
including records, switch expressions/multiple labels, pattern instanceof,
|
||||||
|
lambdas and source-launcher CI classes. Tests compile/run the resulting sources.
|
||||||
|
This demonstrates the syntax used here, not every possible Java 25 construct.
|
||||||
|
- Palantir uses javac internals (explicit JVM exports are in the root POM) and its
|
||||||
|
modern AST visitor. Recheck formatter compatibility when upgrading the JDK.
|
||||||
|
Compact source files / instance main methods are a known upstream limitation
|
||||||
|
([upstream issue](https://github.com/palantir/palantir-java-format/issues/1602));
|
||||||
|
they are not used here. Unsupported future syntax fails, rather than being
|
||||||
|
silently excluded. Extra Markdown/Javadoc formatting is disabled.
|
||||||
|
- The initial Java rewrite is committed separately from integration changes;
|
||||||
|
its only transformations are the formatter's native output and import handling.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
`python3 tooling/format/benchmark.py` runs sequentially, with normal dependencies
|
||||||
|
and OS filesystem caches warm. Each configuration receives a compile warmup.
|
||||||
|
The baseline temporarily removes the entire exec plugin declaration from the
|
||||||
|
root POM, then restores its original bytes before the formatting build. Therefore
|
||||||
|
these comparisons include plugin loading, dependency resolution, JVM launch,
|
||||||
|
source-launcher compilation, hashing, cache IO and recipe execution. They do not
|
||||||
|
compare only the inner formatter function or a skipped plugin against itself.
|
||||||
|
|
||||||
|
The successful-content cache is warm for unchanged builds; it is part of the
|
||||||
|
integration. Changed/new files are processed by Palantir. Clean builds remove the
|
||||||
|
cache and process every maintained Java file. Each run's stdout and elapsed time
|
||||||
|
are saved under `target/`; the benchmark restores the root POM and changed helper.
|
||||||
|
|
||||||
|
| Repeated compile | Baseline (s) | Integration (s) | Added overhead (s) |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| 1 | 4.887 | 6.414 | 1.527 |
|
||||||
|
| 2 | 4.821 | 5.964 | 1.143 |
|
||||||
|
| 3 | 4.559 | 6.024 | 1.465 |
|
||||||
|
|
||||||
|
Maximum observed added overhead: **1.527 seconds**, below the agreed three-second
|
||||||
|
limit. Baseline compile warmup: 5.307 s; integration warmup: 7.604 s.
|
||||||
|
|
||||||
|
The changed-file case appends a newline to `ci/runner/WaitForDocker.java`, then
|
||||||
|
runs `mvn -B -ntp compile`: **5.506 s**, successful; Palantir processes that
|
||||||
|
changed file. The script restores its original bytes afterward.
|
||||||
|
|
||||||
|
Clean baseline `mvn -B -ntp clean verify`: **24.797 s**. Clean integration
|
||||||
|
`mvn -B -ntp -Dformat.mode=check clean verify`: **27.747 s**, all 73 files
|
||||||
|
processed, no differences, all tests pass. The three-second criterion applies
|
||||||
|
to repeated unchanged builds, not the clean-build comparison.
|
||||||
|
|
||||||
|
### First-use downloads (separate measurement)
|
||||||
|
|
||||||
|
A disposable root POM with the same pinned plugin/dependencies and an empty
|
||||||
|
`-Dmaven.repo.local` replaces only the Java executable with `/usr/bin/true`.
|
||||||
|
Thus dependency resolution runs without compiling the launcher or executing the
|
||||||
|
formatter/application. Command:
|
||||||
|
`mvn -N -B -ntp -Dmaven.repo.local=<empty-temp-repository> org.codehaus.mojo:exec-maven-plugin:3.6.3:exec@format`.
|
||||||
|
Cold resolution took **9.041 s**; the same command with that repository populated
|
||||||
|
took **1.826 s**. The **7.215 s** cold-versus-warm difference represents first-use
|
||||||
|
network/repository population overhead in this environment, separately from the
|
||||||
|
warm-cache build measurements above. Cold timing also includes root POM/BOM and
|
||||||
|
plugin bootstrap resolution; it is not reported as pure formatter execution.
|
||||||
|
Reproduce with `python3 tooling/format/benchmark-downloads.py` (Linux, network
|
||||||
|
access). The initial real source-launcher bootstrap/check also successfully
|
||||||
|
processed all files; its expected failure listed formatting differences, rather
|
||||||
|
than a missing project recipe or parser/bootstrap error.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# S8 — Palantir formatting for Java
|
# S8 — Palantir formatting for Java
|
||||||
|
|
||||||
Status: REFINED
|
Status: IN_REVIEW
|
||||||
|
|
||||||
Agreed after the grill-me review; shared understanding confirmed.
|
Agreed after the grill-me review; shared understanding confirmed.
|
||||||
|
|
||||||
@@ -163,30 +163,30 @@ style or silently take precedence over the Maven result.
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] The actual Palantir engine formats all maintained backend Java sources,
|
- [x] The actual Palantir engine formats all maintained backend Java sources,
|
||||||
Java tests and `ci/*.java`; generated code is excluded.
|
Java tests and `ci/*.java`; generated code is excluded.
|
||||||
- [ ] OpenRewrite integration, custom adapter/recipe and bootstrap are implemented
|
- [x] OpenRewrite integration, custom adapter/recipe and bootstrap are implemented
|
||||||
and reproducible from a clean checkout without manual recipe installation.
|
and reproducible from a clean checkout without manual recipe installation.
|
||||||
- [ ] Plugin, formatter and adapter dependencies are pinned, with matching local
|
- [x] Plugin, formatter and adapter dependencies are pinned, with matching local
|
||||||
and CI configuration.
|
and CI configuration.
|
||||||
- [ ] Local Maven builds apply formatting before compilation; `compile`, `test`,
|
- [x] Local Maven builds apply formatting before compilation; `compile`, `test`,
|
||||||
`verify` and `install` reach the intended integration.
|
`verify` and `install` reach the intended integration.
|
||||||
- [ ] Standalone apply and check commands work without the full application build.
|
- [x] Standalone apply and check commands work without the full application build.
|
||||||
- [ ] CI checks only, leaves maintained sources unchanged, reports formatting
|
- [x] CI checks only, leaves maintained sources unchanged, reports formatting
|
||||||
differences and fails before image publication.
|
differences and fails before image publication.
|
||||||
- [ ] Import ordering and unused-import removal follow Palantir; optional extra
|
- [x] Import ordering and unused-import removal follow Palantir; optional extra
|
||||||
Javadoc reformatting remains disabled and no unrelated refactoring is added.
|
Javadoc reformatting remains disabled and no unrelated refactoring is added.
|
||||||
- [ ] All currently used Java 25 syntax is handled, or causes an explicit failure
|
- [x] All currently used Java 25 syntax is handled, or causes an explicit failure
|
||||||
requiring resolution rather than silent exclusion.
|
requiring resolution rather than silent exclusion.
|
||||||
- [ ] Apply/check results agree and a second apply produces no differences.
|
- [x] Apply/check results agree and a second apply produces no differences.
|
||||||
- [ ] Initial formatting is isolated in a formatting-only commit; application
|
- [x] Initial formatting is isolated in a formatting-only commit; application
|
||||||
builds and tests remain green.
|
builds and tests remain green.
|
||||||
- [ ] Equivalent repeated unchanged-build measurements demonstrate at most three
|
- [x] Equivalent repeated unchanged-build measurements demonstrate at most three
|
||||||
seconds of additional integration overhead. Changed-file and clean-build
|
seconds of additional integration overhead. Changed-file and clean-build
|
||||||
measurements and one-time downloads are reported separately.
|
measurements and one-time downloads are reported separately.
|
||||||
- [ ] Failure to meet the performance bound is reported before changing the route
|
- [x] Failure to meet the performance bound is reported before changing the route
|
||||||
or bound; a writing formatter is not killed to force compliance.
|
or bound; a writing formatter is not killed to force compliance.
|
||||||
- [ ] README instructions cover local and CI commands, bootstrap, optional IDE
|
- [x] README instructions cover local and CI commands, bootstrap, optional IDE
|
||||||
use, authoritative Maven formatting and verification evidence.
|
use, authoritative Maven formatting and verification evidence.
|
||||||
|
|
||||||
## Implementation handoff
|
## Implementation handoff
|
||||||
@@ -196,3 +196,5 @@ create an adapter, change Maven/CI behavior or reformat source files. Recipe loa
|
|||||||
Java 25 compatibility and performance must be demonstrated during implementation;
|
Java 25 compatibility and performance must be demonstrated during implementation;
|
||||||
there is no pre-existing project benchmark establishing the preferred route's
|
there is no pre-existing project benchmark establishing the preferred route's
|
||||||
performance.
|
performance.
|
||||||
|
|
||||||
|
Implementation and measured acceptance evidence: [S8-formatting-evidence.md](../S8-formatting-evidence.md).
|
||||||
131
tooling/format/Format.java
Normal file
131
tooling/format/Format.java
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import com.palantir.javaformat.java.Formatter;
|
||||||
|
import com.palantir.javaformat.java.JavaFormatterOptions;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.*;
|
||||||
|
import org.openrewrite.*;
|
||||||
|
import org.openrewrite.internal.InMemoryLargeSourceSet;
|
||||||
|
import org.openrewrite.text.*;
|
||||||
|
|
||||||
|
/** Source-launcher bootstrap: no project-specific artifact is resolved or installed. */
|
||||||
|
public class Format {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
Path root = Path.of(args[0]).toAbsolutePath().normalize();
|
||||||
|
boolean check = switch (args[1]) {
|
||||||
|
case "check" -> true;
|
||||||
|
case "apply" -> false;
|
||||||
|
default -> throw new IllegalArgumentException("format.mode must be apply or check");
|
||||||
|
};
|
||||||
|
Path cacheFile = root.resolve("target/palantir-cache.properties");
|
||||||
|
var cache = new Properties();
|
||||||
|
if (Files.exists(cacheFile)) {
|
||||||
|
try (var input = Files.newInputStream(cacheFile)) {
|
||||||
|
cache.load(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String configuration = digest(Files.readString(root.resolve("pom.xml"))
|
||||||
|
+ Files.readString(root.resolve("tooling/format/Format.java"))
|
||||||
|
+ System.getProperty("java.version")
|
||||||
|
+ System.getProperty("java.class.path"));
|
||||||
|
if (!configuration.equals(cache.getProperty("configuration"))) cache.clear();
|
||||||
|
cache.setProperty("configuration", configuration);
|
||||||
|
List<SourceFile> sources = new ArrayList<>();
|
||||||
|
for (String directory :
|
||||||
|
List.of("agiledancers-be/src/main/java", "agiledancers-be/src/test/java", "ci", "tooling/format")) {
|
||||||
|
try (var paths = Files.walk(root.resolve(directory))) {
|
||||||
|
for (Path path : paths.filter(p -> p.toString().endsWith(".java"))
|
||||||
|
.sorted()
|
||||||
|
.toList()) {
|
||||||
|
try {
|
||||||
|
String content = Files.readString(path);
|
||||||
|
String key = root.relativize(path).toString();
|
||||||
|
if (!digest(content).equals(cache.getProperty(key))) {
|
||||||
|
sources.add(PlainText.builder()
|
||||||
|
.sourcePath(root.relativize(path))
|
||||||
|
.text(content)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
} catch (Exception error) {
|
||||||
|
throw new IllegalStateException(path + ": " + error.getMessage(), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var context = new InMemoryExecutionContext(error -> {
|
||||||
|
throw new IllegalStateException("Formatting failed: " + error.getMessage(), error);
|
||||||
|
});
|
||||||
|
var results = new PalantirRecipe()
|
||||||
|
.run(new InMemoryLargeSourceSet(sources), context)
|
||||||
|
.getChangeset()
|
||||||
|
.getAllResults();
|
||||||
|
for (Result result : results) {
|
||||||
|
System.out.println((check ? "Needs formatting: " : "Formatted: ")
|
||||||
|
+ result.getAfter().getSourcePath());
|
||||||
|
}
|
||||||
|
if (check && !results.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Run mvn -N exec:exec@format to apply Palantir formatting.");
|
||||||
|
}
|
||||||
|
// Complete parsing/formatting before writing any maintained file. Each replacement is atomic.
|
||||||
|
if (!check) {
|
||||||
|
for (Result result : results) {
|
||||||
|
Path destination = root.resolve(result.getAfter().getSourcePath());
|
||||||
|
Path temporary = Files.createTempFile(destination.getParent(), ".palantir-", ".tmp");
|
||||||
|
try {
|
||||||
|
Files.writeString(temporary, result.getAfter().printAll());
|
||||||
|
Files.setPosixFilePermissions(temporary, Files.getPosixFilePermissions(destination));
|
||||||
|
Files.move(
|
||||||
|
temporary,
|
||||||
|
destination,
|
||||||
|
StandardCopyOption.ATOMIC_MOVE,
|
||||||
|
StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SourceFile source : sources) {
|
||||||
|
cache.setProperty(
|
||||||
|
source.getSourcePath().toString(), digest(Files.readString(root.resolve(source.getSourcePath()))));
|
||||||
|
}
|
||||||
|
Files.createDirectories(cacheFile.getParent());
|
||||||
|
try (var output = Files.newOutputStream(cacheFile)) {
|
||||||
|
cache.store(output, "Successfully formatted content only");
|
||||||
|
}
|
||||||
|
System.out.println(
|
||||||
|
"Palantir " + args[1] + ": " + sources.size() + " Java files, " + results.size() + " differences");
|
||||||
|
}
|
||||||
|
|
||||||
|
static String digest(String value) throws Exception {
|
||||||
|
return HexFormat.of()
|
||||||
|
.formatHex(MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class PalantirRecipe extends Recipe {
|
||||||
|
public String getDisplayName() {
|
||||||
|
return "Palantir Java Format";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return "Apply the pinned Palantir engine and its native import handling.";
|
||||||
|
}
|
||||||
|
|
||||||
|
public TreeVisitor<?, ExecutionContext> getVisitor() {
|
||||||
|
return new PlainTextVisitor<ExecutionContext>() {
|
||||||
|
final Formatter formatter = Formatter.createFormatter(JavaFormatterOptions.builder()
|
||||||
|
.style(JavaFormatterOptions.Style.PALANTIR)
|
||||||
|
.formatJavadoc(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlainText visitText(PlainText text, ExecutionContext context) {
|
||||||
|
try {
|
||||||
|
return text.withText(formatter.formatSourceAndFixImports(text.getText()));
|
||||||
|
} catch (Exception | LinkageError error) {
|
||||||
|
throw new IllegalStateException(text.getSourcePath() + ": " + error, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
tooling/format/benchmark-downloads.py
Normal file
31
tooling/format/benchmark-downloads.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""Measure dependency resolution separately; run from the root with network access.
|
||||||
|
|
||||||
|
A disposable copy replaces the Java executable with /usr/bin/true, keeping the
|
||||||
|
pinned exec-plugin dependencies. No formatter/application code executes. The
|
||||||
|
second invocation uses the populated repository; neither invocation touches the
|
||||||
|
normal Maven cache. Logs and measurements are written under target/.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
(root / "target").mkdir(exist_ok=True)
|
||||||
|
with tempfile.TemporaryDirectory(prefix="s8-resolution-") as temporary:
|
||||||
|
fixture = Path(temporary)
|
||||||
|
shutil.copytree(root / ".mvn", fixture / ".mvn")
|
||||||
|
pom = (root / "pom.xml").read_text().replace("${java.home}/bin/java", "/usr/bin/true")
|
||||||
|
(fixture / "pom.xml").write_text(pom)
|
||||||
|
command = ["mvn", "-N", "-B", "-ntp", "-Dmaven.repo.local=" + str(fixture / "repository"),
|
||||||
|
"org.codehaus.mojo:exec-maven-plugin:3.6.3:exec@format"]
|
||||||
|
for label in ("cold-resolution", "warm-resolution"):
|
||||||
|
start = time.monotonic()
|
||||||
|
result = subprocess.run(command, cwd=fixture, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
|
elapsed = time.monotonic() - start
|
||||||
|
(root / "target" / ("s8-" + label + ".log")).write_text(result.stdout)
|
||||||
|
print(json.dumps(dict(label=label, seconds=round(elapsed, 3), exit=result.returncode)), flush=True)
|
||||||
|
if result.returncode:
|
||||||
|
raise RuntimeError(result.stdout)
|
||||||
49
tooling/format/benchmark.py
Normal file
49
tooling/format/benchmark.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""Run sequential, warm-cache end-to-end Maven comparisons from the repository root."""
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
results = []
|
||||||
|
pom = root / "pom.xml"
|
||||||
|
original_pom = pom.read_bytes()
|
||||||
|
pom_text = original_pom.decode()
|
||||||
|
start = pom_text.rfind(" <plugin>", 0, pom_text.index("<artifactId>exec-maven-plugin</artifactId>"))
|
||||||
|
end = pom_text.index(" </plugin>", start) + len(" </plugin>\n")
|
||||||
|
baseline_pom = (pom_text[:start] + pom_text[end:]).encode()
|
||||||
|
|
||||||
|
def run(label, *arguments):
|
||||||
|
command = ["mvn", "-B", "-ntp", *arguments]
|
||||||
|
start = time.monotonic()
|
||||||
|
baseline = "baseline" in label
|
||||||
|
try:
|
||||||
|
if baseline:
|
||||||
|
pom.write_bytes(baseline_pom)
|
||||||
|
result = subprocess.run(command, cwd=root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
|
finally:
|
||||||
|
pom.write_bytes(original_pom)
|
||||||
|
elapsed = time.monotonic() - start
|
||||||
|
(root / "target").mkdir(exist_ok=True)
|
||||||
|
(root / "target" / ("s8-" + label + ".log")).write_text(result.stdout)
|
||||||
|
row = dict(label=label, command=command, seconds=round(elapsed, 3), exit=result.returncode)
|
||||||
|
results.append(row)
|
||||||
|
print(json.dumps(row), flush=True)
|
||||||
|
if result.returncode:
|
||||||
|
raise RuntimeError(result.stdout)
|
||||||
|
|
||||||
|
run("warmup", "compile")
|
||||||
|
run("baseline-warmup", "compile")
|
||||||
|
for number in range(3):
|
||||||
|
run("baseline-" + str(number), "compile")
|
||||||
|
run("formatted-" + str(number), "compile")
|
||||||
|
source = root / "ci/runner/WaitForDocker.java"
|
||||||
|
original = source.read_bytes()
|
||||||
|
try:
|
||||||
|
source.write_bytes(original + b"\n")
|
||||||
|
run("changed-file", "compile")
|
||||||
|
finally:
|
||||||
|
source.write_bytes(original)
|
||||||
|
run("clean-baseline", "clean", "verify")
|
||||||
|
run("clean-formatted", "-Dformat.mode=check", "clean", "verify")
|
||||||
|
(root / "target/s8-benchmark.json").write_text(json.dumps(results, indent=2) + "\n")
|
||||||
53
tooling/format/test.py
Normal file
53
tooling/format/test.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""Integration checks against an isolated checkout; run python3 tooling/format/test.py."""
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
with tempfile.TemporaryDirectory(prefix="palantir-test-") as temporary:
|
||||||
|
fixture = Path(temporary)
|
||||||
|
for name in ("pom.xml", ".mvn", "tooling/format"):
|
||||||
|
source = root / name
|
||||||
|
destination = fixture / name
|
||||||
|
if source.is_dir():
|
||||||
|
shutil.copytree(source, destination)
|
||||||
|
else:
|
||||||
|
shutil.copy2(source, destination)
|
||||||
|
for name in ("agiledancers-be/src/main/java", "agiledancers-be/src/test/java", "ci/runner"):
|
||||||
|
(fixture / name).mkdir(parents=True)
|
||||||
|
source = fixture / "ci/runner/Example.java"
|
||||||
|
original = "import java.util.List;\nclass Example{String text=\"héllo\";int value(){return 1;}}\n"
|
||||||
|
source.write_text(original)
|
||||||
|
|
||||||
|
def run(mode, success=True):
|
||||||
|
result = subprocess.run(
|
||||||
|
["mvn", "-N", "-B", "-ntp", "exec:exec@format", "-Dformat.mode=" + mode],
|
||||||
|
cwd=fixture, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
assert (result.returncode == 0) == success, result.stdout
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
output = run("check", False)
|
||||||
|
assert "ci/runner/Example.java" in output and "mvn -N exec:exec@format" in output
|
||||||
|
assert source.read_text() == original
|
||||||
|
run("apply")
|
||||||
|
formatted = source.read_text()
|
||||||
|
assert "import java.util.List" not in formatted and "héllo" in formatted
|
||||||
|
run("check")
|
||||||
|
run("apply")
|
||||||
|
assert source.read_text() == formatted
|
||||||
|
# Force a full (uncached) second pass to prove engine idempotence.
|
||||||
|
(fixture / "target/palantir-cache.properties").unlink()
|
||||||
|
run("check")
|
||||||
|
source.write_text(formatted + "class Broken { ??? }\n")
|
||||||
|
invalid = source.read_bytes()
|
||||||
|
output = run("apply", False)
|
||||||
|
assert "ci/runner/Example.java" in output
|
||||||
|
assert source.read_bytes() == invalid
|
||||||
|
# A new file is discovered even after a successful cached check.
|
||||||
|
source.write_text(formatted)
|
||||||
|
extra = fixture / "ci/New.java"
|
||||||
|
extra.write_text("class New{}")
|
||||||
|
run("check", False)
|
||||||
|
assert extra.read_text() == "class New{}"
|
||||||
|
print("PASS: check is read-only, apply/check agree, imports, UTF-8, idempotence, parser errors, new files")
|
||||||
Reference in New Issue
Block a user