blob: 13b18c9ee8ceaee4f6a1da96b1de4945d6d9ebea [file]
#!/usr/bin/env python3
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Headless reproduction of Eclipse JDT compiler warnings via `ecj`.
`ecj` is the exact JDT compiler Eclipse embeds. Driven by a module's own
.settings/org.eclipse.jdt.core.prefs (the file scripts/apply-eclipse-prefs.py
seeds/syncs), it reproduces Eclipse's Problems-view diagnostics verbatim,
including exact message strings -- without a live Eclipse session or the
AssistAI Eclipse MCP server. See the `eclipse-warnings` skill's "Headless
fallback" section for the full technique writeup and when to prefer it.
Usage (run from the repo root):
python3 scripts/eclipse-warnings.py <module-path> # e.g. juneau-rest/juneau-rest-server
python3 scripts/eclipse-warnings.py --all # every source module apply-eclipse-prefs.py would touch
Requires:
- `ecj` 3.45.0. Resolved automatically from ~/.m2/repository/org/eclipse/jdt/ecj/3.45.0/
(already present if the project has ever built; the ecj Maven coordinate is
org.eclipse.jdt:ecj:3.45.0). Override with the ECJ_JAR env var if it lives elsewhere.
- the module's own .settings/org.eclipse.jdt.core.prefs to exist (run
apply-eclipse-prefs.py first if missing/stale).
- `mvn` on PATH, to resolve the module's runtime+test classpath.
Javadoc diagnostics additionally need -enableJavadoc plus explicit javadoc
severities (passed below unconditionally) because the repo's tracked .prefs
files set no javadoc options at all -- those particular diagnostics come from
workspace-level Eclipse settings that no tracked file controls. Omitting the
flags means a javadoc warning Eclipse reports simply won't reproduce here.
Caveat -- classpath uses INSTALLED reactor jars, not live sources: the runtime
classpath comes from `mvn dependency:build-classpath`, which resolves other
reactor modules' jars from ~/.m2 as of their last `mvn install`, not their
current on-disk source. A class added/changed in an upstream module since the
last install won't be visible here ("X cannot be resolved" for a genuinely
new/renamed type is the tell) -- `mvn install` the affected upstream module(s)
first. Also expect occasional "invalid Class-Path header in manifest of jar
file ..." lines from ecj scanning a third-party dependency's own bundled
manifest; that's noise unrelated to Juneau's code and safe to ignore.
"""
import glob
import importlib.util
import os
import subprocess
import sys
import tempfile
from pathlib import Path
DEFAULT_ECJ_JAR = Path.home() / ".m2/repository/org/eclipse/jdt/ecj/3.45.0/ecj-3.45.0.jar"
def find_ecj_jar():
override = os.environ.get("ECJ_JAR")
if override:
return Path(override)
return DEFAULT_ECJ_JAR
def load_apply_prefs_module(script_dir):
"""Reuse apply-eclipse-prefs.py's module discovery so both scripts agree
on what counts as a module and how it's classified source vs. test."""
spec = importlib.util.spec_from_file_location(
"apply_eclipse_prefs", script_dir / "apply-eclipse-prefs.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def build_classpath(module_dir):
"""Run `mvn dependency:build-classpath` from inside the module dir itself
(not `-pl <module>` from the reactor root). `-pl` still triggers a
multi-module reactor pass, and multiple modules' plugin executions racing
to write the same `-Dmdep.outputFile` target corrupts the output (jar
paths get concatenated with missing `:` separators). Running from the
module's own directory keeps this to a single-module build."""
with tempfile.NamedTemporaryFile(prefix="ecj-cp-", suffix=".txt", delete=False) as tmp:
cp_file = tmp.name
try:
result = subprocess.run(
["mvn", "-q", "dependency:build-classpath", f"-Dmdep.outputFile={cp_file}"],
cwd=module_dir, capture_output=True, text=True, timeout=180,
)
if result.returncode != 0:
print(f"Warning: `mvn dependency:build-classpath` failed for {module_dir}:", file=sys.stderr)
print(result.stderr, file=sys.stderr)
return ""
return Path(cp_file).read_text().strip()
finally:
os.unlink(cp_file)
def java_sources(module_dir):
files = []
for sub in ("src/main/java", "src/test/java"):
src_dir = module_dir / sub
if src_dir.exists():
files.extend(glob.glob(str(src_dir / "**" / "*.java"), recursive=True))
return files
def run_ecj(ecj_jar, module_dir, module_path, classpath):
prefs = module_dir / ".settings" / "org.eclipse.jdt.core.prefs"
if not prefs.exists():
print(f"Skipping {module_path}: no .settings/org.eclipse.jdt.core.prefs "
f"(run apply-eclipse-prefs.py first)", file=sys.stderr)
return None
sources = java_sources(module_dir)
if not sources:
print(f"Skipping {module_path}: no .java sources found", file=sys.stderr)
return None
target_classes = module_dir / "target" / "classes"
test_classes = module_dir / "target" / "test-classes"
cp_parts = [classpath] if classpath else []
for extra in (target_classes, test_classes):
if extra.exists():
cp_parts.append(str(extra))
full_cp = os.pathsep.join(p for p in cp_parts if p)
with tempfile.TemporaryDirectory(prefix="ecj-out-") as out_dir:
cmd = [
"java", "-jar", str(ecj_jar),
"-properties", str(prefs),
"-enableJavadoc", "-warn:+allJavadoc,invalidJavadoc,javadoc",
"-cp", full_cp,
"-d", out_dir,
] + sources
result = subprocess.run(cmd, cwd=module_dir, capture_output=True, text=True, timeout=300)
return result.stdout + result.stderr
def summarize(output):
"""Pull ecj's trailing '<n> problems (<e> errors, <w> warnings)' line, if present."""
for line in reversed(output.strip().splitlines()):
if "problem" in line and ("error" in line or "warning" in line):
return line.strip()
return None
def main():
script_dir = Path(__file__).parent
root_dir = script_dir.parent
if len(sys.argv) != 2:
print(__doc__)
sys.exit(1)
ecj_jar = find_ecj_jar()
if not ecj_jar.exists():
print(f"ecj jar not found at {ecj_jar}.", file=sys.stderr)
print("Resolve it first, e.g.: mvn dependency:get -Dartifact=org.eclipse.jdt:ecj:3.45.0", file=sys.stderr)
print("or set ECJ_JAR to point at an existing copy.", file=sys.stderr)
sys.exit(2)
if sys.argv[1] == "--all":
apply_prefs = load_apply_prefs_module(script_dir)
source_modules, test_modules = apply_prefs.discover_projects(root_dir)
module_paths = source_modules + test_modules
else:
module_paths = [sys.argv[1]]
any_problems = False
for module_path in module_paths:
module_dir = root_dir / module_path
if not module_dir.exists():
print(f"Warning: module not found: {module_path}", file=sys.stderr)
continue
print(f"\n{'=' * 60}\n{module_path}\n{'=' * 60}")
classpath = build_classpath(module_dir)
output = run_ecj(ecj_jar, module_dir, module_path, classpath)
if output is None:
continue
print(output)
summary = summarize(output)
if summary:
print(f">>> {module_path}: {summary}")
if "0 errors, 0 warnings" not in summary:
any_problems = True
sys.exit(1 if any_problems else 0)
if __name__ == '__main__':
main()