From c7d96f3d83ff4cbe3d1bb28b6979f610ef3bb008 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Tue, 18 Nov 2025 09:47:24 +0800 Subject: [PATCH] added finalguard plugin in aplha state --- build.gradle | 19 +- finalguard/build.gradle | 12 ++ .../finalguard-javac-plugin/build.gradle | 38 ++++ .../woggioni/finalguard/FinalGuardPlugin.java | 178 +++++++++++++++++ .../services/com.sun.source.util.Plugin | 1 + .../net/woggioni/finalguard/PluginTest.java | 183 ++++++++++++++++++ .../woggioni/finalguard/test/TestCase1.java | 26 +++ .../woggioni/finalguard/test/TestCase2.java | 18 ++ .../woggioni/finalguard/test/TestCase3.java | 6 + .../woggioni/finalguard/test/TestCase4.java | 7 + .../woggioni/finalguard/test/TestCase5.java | 8 + .../woggioni/finalguard/test/TestCase6.java | 9 + .../woggioni/finalguard/test/TestCase7.java | 10 + .../woggioni/finalguard/test/TestCase8.java | 13 ++ .../finalguard/FinalGuardExtension.java | 10 + .../gradle/finalguard/FinalGuardPlugin.java | 61 ++++++ gradle.properties | 4 +- settings.gradle | 2 + 18 files changed, 599 insertions(+), 6 deletions(-) create mode 100644 finalguard/build.gradle create mode 100644 finalguard/finalguard-javac-plugin/build.gradle create mode 100644 finalguard/finalguard-javac-plugin/src/main/java/net/woggioni/finalguard/FinalGuardPlugin.java create mode 100644 finalguard/finalguard-javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin create mode 100644 finalguard/finalguard-javac-plugin/src/test/java/net/woggioni/finalguard/PluginTest.java create mode 100644 finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase1.java create mode 100644 finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase2.java create mode 100644 finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase3.java create mode 100644 finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase4.java create mode 100644 finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase5.java create mode 100644 finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase6.java create mode 100644 finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase7.java create mode 100644 finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase8.java create mode 100644 finalguard/src/main/java/net/woggioni/gradle/finalguard/FinalGuardExtension.java create mode 100644 finalguard/src/main/java/net/woggioni/gradle/finalguard/FinalGuardPlugin.java diff --git a/build.gradle b/build.gradle index 135b503..4826bd2 100644 --- a/build.gradle +++ b/build.gradle @@ -11,15 +11,25 @@ subprojects { subproject -> } } + tasks.named(JavaPlugin.JAR_TASK_NAME, Jar.class, { + manifest { + attributes.put(java.util.jar.Attributes.Name.SPECIFICATION_TITLE.toString(), subproject.getName()); + attributes.put(java.util.jar.Attributes.Name.SPECIFICATION_VERSION.toString(), subproject.getVersion()); + } + }) + int javaVersion if(subproject.path == ':osgi-app' || subproject.path == ':multi-release-jar') { javaVersion = 11 } else { javaVersion = 8 } - tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { - options.release = javaVersion - options.compilerArgs << '-parameters' + + if(subproject.path != ':finalguard:finalguard-javac-plugin') { + tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { + options.release = javaVersion + options.compilerArgs << '-parameters' + } } pluginManager.withPlugin('groovy') { @@ -37,7 +47,9 @@ subprojects { subproject -> add(conf, [group: "org.projectlombok", name: "lombok", version: catalog.versions.lombok.get()]) } add("testImplementation", catalog.junit.jupiter.api) + add("testImplementation", catalog.junit.jupiter.params) add("testRuntimeOnly", catalog.junit.jupiter.engine) + add("testRuntimeOnly", catalog.junit.platform.launcher) add("testImplementation", gradleTestKit()) } @@ -64,7 +76,6 @@ subprojects { subproject -> } } - wrapper { gradleVersion = getProperty("version.gradle") distributionType = Wrapper.DistributionType.ALL diff --git a/finalguard/build.gradle b/finalguard/build.gradle new file mode 100644 index 0000000..78abc3f --- /dev/null +++ b/finalguard/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java-gradle-plugin' +} + +gradlePlugin { + plugins { + create("FinalGuardPlugin") { + id = 'net.woggioni.gradle.finalguard' + implementationClass = "net.woggioni.gradle.finalguard.FinalGuardPlugin" + } + } +} diff --git a/finalguard/finalguard-javac-plugin/build.gradle b/finalguard/finalguard-javac-plugin/build.gradle new file mode 100644 index 0000000..a054ccc --- /dev/null +++ b/finalguard/finalguard-javac-plugin/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'java-library' +} + +group = "net.woggioni.finalguard" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +java { + sourceCompatibility(JavaVersion.VERSION_1_8.toString()) + targetCompatibility(JavaVersion.VERSION_1_8.toString()) + modularity.inferModulePath = false +} + +tasks.named(org.gradle.api.plugins.JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile.class) { + options.compilerArgs << '-parameters' +} + +test { + def testCompilationClassPath = sourceSets["main"].output.classesDirs.files + + sourceSets["main"].runtimeClasspath.files + + sourceSets["test"].resources.srcDirs + systemProperty("test.compilation.classpath", + String.join(File.pathSeparator, testCompilationClassPath.collect {it.toString() })) +} + + +publishing { + publications { + maven(MavenPublication) { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/main/java/net/woggioni/finalguard/FinalGuardPlugin.java b/finalguard/finalguard-javac-plugin/src/main/java/net/woggioni/finalguard/FinalGuardPlugin.java new file mode 100644 index 0000000..67aeeb8 --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/main/java/net/woggioni/finalguard/FinalGuardPlugin.java @@ -0,0 +1,178 @@ +package net.woggioni.finalguard; + +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.Plugin; +import com.sun.source.util.TaskEvent; +import com.sun.source.util.TaskListener; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import com.sun.source.util.Trees; + +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.tools.Diagnostic; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class FinalGuardPlugin implements Plugin { + + public static final String DIAGNOSTIC_LEVEL_KEY = "net.woggioni.finalguard.diagnostic.level"; + + private static final Diagnostic.Kind diagnosticLevel = + Optional.ofNullable(System.getProperty(DIAGNOSTIC_LEVEL_KEY)) + .map(Diagnostic.Kind::valueOf) + .orElse(Diagnostic.Kind.WARNING); + + @Override + public String getName() { + return getClass().getName(); + } + + @Override + public void init(JavacTask task, String... args) { + task.addTaskListener(new TaskListener() { + @Override + public void started(TaskEvent e) {} + + @Override + public void finished(TaskEvent e) { + if (e.getKind() == TaskEvent.Kind.ANALYZE) { + analyzeFinalVariables(e.getCompilationUnit(), task, e.getTypeElement()); + } + } + }); + } + + private void analyzeFinalVariables(CompilationUnitTree compilationUnit, JavacTask task, Element typeElement) { + FinalVariableAnalyzer analyzer = new FinalVariableAnalyzer(compilationUnit, task); + TreePath path = Trees.instance(task).getPath(typeElement); + if(path != null) { + analyzer.scan(path, null); + } + } + + private static class FinalVariableAnalyzer extends TreePathScanner { + private final CompilationUnitTree compilationUnit; + private final Trees trees; + private final Map variableInfoMap = new HashMap<>(); + private final Set reassignedVariables = new HashSet<>(); + private String currentMethod; + + public FinalVariableAnalyzer(CompilationUnitTree compilationUnit, JavacTask task) { + this.compilationUnit = compilationUnit; + this.trees = Trees.instance(task); + } + + @Override + public Void visitMethod(MethodTree node, Void p) { + String previousMethod = currentMethod; + currentMethod = node.getName().toString(); + variableInfoMap.clear(); + reassignedVariables.clear(); + + // Analyze parameters first + for (VariableTree param : node.getParameters()) { + String varName = param.getName().toString(); + variableInfoMap.put(varName, new VariableInfo(param, false)); + } + + // Then analyze method body + super.visitMethod(node, p); + + // Check for variables that could be final + checkForFinalCandidates(); + + currentMethod = previousMethod; + return null; + } + + @Override + public Void visitVariable(VariableTree node, Void p) { + if (currentMethod != null) { + String varName = node.getName().toString(); + boolean isParameter = node.getKind() == Tree.Kind.METHOD; + variableInfoMap.put(varName, new VariableInfo(node, isParameter)); + } + return super.visitVariable(node, p); + } + + @Override + public Void visitAssignment(AssignmentTree node, Void p) { + if (node.getVariable() instanceof IdentifierTree) { + IdentifierTree ident = (IdentifierTree) node.getVariable(); + reassignedVariables.add(ident.getName().toString()); + } + return super.visitAssignment(node, p); + } + + @Override + public Void visitUnary(UnaryTree node, Void p) { + if ((node.getKind() == Tree.Kind.PREFIX_INCREMENT || + node.getKind() == Tree.Kind.PREFIX_DECREMENT || + node.getKind() == Tree.Kind.POSTFIX_INCREMENT || + node.getKind() == Tree.Kind.POSTFIX_DECREMENT) && + node.getExpression() instanceof IdentifierTree) { + + IdentifierTree ident = (IdentifierTree) node.getExpression(); + reassignedVariables.add(ident.getName().toString()); + } + return super.visitUnary(node, p); + } + + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { + if (node.getVariable() instanceof IdentifierTree) { + IdentifierTree ident = (IdentifierTree) node.getVariable(); + reassignedVariables.add(ident.getName().toString()); + } + return super.visitCompoundAssignment(node, p); + } + + private void checkForFinalCandidates() { + for (Map.Entry entry : variableInfoMap.entrySet()) { + String varName = entry.getKey(); + VariableInfo info = entry.getValue(); + + // Skip if already final + if (isFinal(info.variableTree)) { + continue; + } + + // Skip if reassigned + if (reassignedVariables.contains(varName)) { + continue; + } + + String message = "Local variable '" + varName + "' is never reassigned, so it should be declared final"; + trees.printMessage(FinalGuardPlugin.diagnosticLevel, + message, + info.variableTree, + compilationUnit); + } + } + + private static boolean isFinal(VariableTree variableTree) { + Set modifiers = variableTree.getModifiers().getFlags(); + return modifiers.contains(Modifier.FINAL); + } + } + + private static class VariableInfo { + final VariableTree variableTree; + + VariableInfo(VariableTree variableTree, boolean isParameter) { + this.variableTree = variableTree; + } + } +} \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin b/finalguard/finalguard-javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin new file mode 100644 index 0000000..518d432 --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin @@ -0,0 +1 @@ +net.woggioni.finalguard.FinalGuardPlugin \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/test/java/net/woggioni/finalguard/PluginTest.java b/finalguard/finalguard-javac-plugin/src/test/java/net/woggioni/finalguard/PluginTest.java new file mode 100644 index 0000000..bb71217 --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/test/java/net/woggioni/finalguard/PluginTest.java @@ -0,0 +1,183 @@ +package net.woggioni.finalguard; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class PluginTest { + + private static class ClassFile extends SimpleJavaFileObject { + + private ByteArrayOutputStream out; + + public ClassFile(URI uri) { + super(uri, Kind.CLASS); + } + + @Override + public OutputStream openOutputStream() { + return out = new ByteArrayOutputStream(); + } + + public byte[] getCompiledBinaries() { + return out.toByteArray(); + } + } + + private static class SourceFile extends SimpleJavaFileObject { + public SourceFile(URI uri) { + super(uri, Kind.SOURCE); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + Reader r = new InputStreamReader(uri.toURL().openStream()); + StringBuilder sb = new StringBuilder(); + char[] buffer = new char[0x1000]; + while (true) { + int read = r.read(buffer); + if(read < 0) break; + sb.append(buffer, 0, read); + } + return sb.toString(); + } + } + + private static class FileManager extends ForwardingJavaFileManager { + + private final List compiled = new ArrayList<>(); + + protected FileManager(StandardJavaFileManager fileManager) { + super(fileManager); + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, + String className, + JavaFileObject.Kind kind, + FileObject sibling) { + ClassFile result = new ClassFile(URI.create("string://" + className)); + compiled.add(result); + return result; + } + + public List getCompiled() { + return compiled; + } + } + + private Optional>> compile(Iterable sources) { + StringWriter output = new StringWriter(); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + FileManager fileManager = + new FileManager(compiler.getStandardFileManager(null, null, null)); + List compilationUnits = StreamSupport.stream(sources.spliterator(), false) + .map(SourceFile::new).collect(Collectors.toList()); + List arguments = Arrays.asList( + "-classpath", System.getProperty("test.compilation.classpath"), + "-Xplugin:" + FinalGuardPlugin.class.getSimpleName() + ); + final ArrayList> compilerMessages = new ArrayList<>(); + System.setProperty(FinalGuardPlugin.DIAGNOSTIC_LEVEL_KEY, "ERROR"); + JavaCompiler.CompilationTask task = compiler.getTask( + output, + fileManager, + compilerMessages::add, + arguments, + null, + compilationUnits + ); + if(task.call()) return Optional.empty(); + else return Optional.of(compilerMessages); + } + + private enum CompilationResult { + SUCCESS, FAILURE + } + + private static class TestCaseProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + String prefix = "net/woggioni/finalguard/test/"; + return Stream.of( + Arguments.of(prefix + "TestCase1.java", CompilationResult.FAILURE), + Arguments.of(prefix + "TestCase2.java", CompilationResult.SUCCESS), + Arguments.of(prefix + "TestCase3.java", CompilationResult.FAILURE), + Arguments.of(prefix + "TestCase4.java", CompilationResult.FAILURE), + Arguments.of(prefix + "TestCase5.java", CompilationResult.FAILURE), + Arguments.of(prefix + "TestCase6.java", CompilationResult.FAILURE), + Arguments.of(prefix + "TestCase7.java", CompilationResult.FAILURE), + Arguments.of(prefix + "TestCase8.java", CompilationResult.FAILURE) + ); + } + } + + @Disabled + @ParameterizedTest(name="{0}") + @ArgumentsSource(TestCaseProvider.class) + public void test(String sourceFilePath, CompilationResult expectedCompilationResult) { + Optional>> result; + try { + ClassLoader cl = getClass().getClassLoader(); + result = compile(Collections.singletonList(cl.getResource(sourceFilePath).toURI())); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + result.ifPresent(diagnostics -> { + for(Diagnostic diagnostic : diagnostics) { + System.err.printf("%s:%s %s\n", + diagnostic.getSource().getName(), + diagnostic.getLineNumber(), + diagnostic.getMessage(Locale.getDefault())); + } + }); + final Pattern errorMessage = Pattern.compile( + "Local variable '[a-zA-Z0-9_]+' is never reassigned, so it should be declared final"); + switch (expectedCompilationResult) { + case SUCCESS: + Assertions.assertFalse(result.isPresent(), "Compilation was expected to succeed but it has failed"); + break; + case FAILURE: + Assertions.assertTrue(result.isPresent(), "Compilation was expected to fail but it succeeded"); + + Optional> illegalSuspendableInvocationMessage = StreamSupport.stream( + result.get().spliterator(), false) + .filter( diagnostic -> + errorMessage.matcher(diagnostic.getMessage(Locale.ENGLISH)).matches() + ).findFirst(); + Assertions.assertTrue(illegalSuspendableInvocationMessage.isPresent()); + break; + } + } +} diff --git a/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase1.java b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase1.java new file mode 100644 index 0000000..09e90eb --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase1.java @@ -0,0 +1,26 @@ +import java.util.Arrays; + +public class TestCase1 { + + public void testMethod(String param1, String param2) { // Warning: param1 could be final + String localVar = "hello"; // Warning: localVar could be final + String reassignedVar = "initial"; // No warning (reassigned below) + reassignedVar = "changed"; + + param2 = "modified"; // No warning for param2 (reassigned) + + for (int i = 0; i < 10; i++) { // Warning: i could be final + String loopVar = "constant"; // Warning: loopVar could be final + } + + // Enhanced for loop - no warning for loop variable + for (String item : Arrays.asList("a", "b")) { + // item is effectively final in each iteration + } + } + + public void finalMethod(final String param1, String param2) { // Warning only for param2 + final String localVar = "hello"; // No warning (already final) + String anotherVar = "world"; // Warning: anotherVar could be final + } +} \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase2.java b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase2.java new file mode 100644 index 0000000..ddc7ec6 --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase2.java @@ -0,0 +1,18 @@ +public class TestCase2 { + + public void testMethod(int a, int b, int c, int d, int e, int f, int g, int h, int j, int k, int l, int m, int n) { + a++; + b--; + ++c; + --d; + e |= 2; + f &= 1; + g <<= 1; + h >>= 1; + j ^= 1; + k += 1; + l -= 1; + m *= 1; + n /= 1; + } +} \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase3.java b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase3.java new file mode 100644 index 0000000..fbd1429 --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase3.java @@ -0,0 +1,6 @@ +public class TestCase3 { + + public void testMethod() { + int n = 5; // Error: n could be final + } +} \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase4.java b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase4.java new file mode 100644 index 0000000..72ebc2c --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase4.java @@ -0,0 +1,7 @@ +public class TestCase4 { + + public void testMethod() { + for (int i = 0; i < 10;) { // Error: i could be final + } + } +} \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase5.java b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase5.java new file mode 100644 index 0000000..a213d7e --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase5.java @@ -0,0 +1,8 @@ +public class TestCase5 { + + public void testMethod() { + for (int i = 0; i < 10; i++) { + String loopVar = "constant"; // Error: loopVar should be final + } + } +} \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase6.java b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase6.java new file mode 100644 index 0000000..3b8d9a6 --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase6.java @@ -0,0 +1,9 @@ +import java.util.Arrays; + +public class TestCase6 { + + public void testMethod() { + for (String item : Arrays.asList("a", "b")) { // Error: item should be final + } + } +} \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase7.java b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase7.java new file mode 100644 index 0000000..31196b4 --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase7.java @@ -0,0 +1,10 @@ +public class TestCase7 { + + public void testMethod() { + try { + + } catch (RuntimeException re) { // Error: re should be final + + } + } +} \ No newline at end of file diff --git a/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase8.java b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase8.java new file mode 100644 index 0000000..d6e24ba --- /dev/null +++ b/finalguard/finalguard-javac-plugin/src/test/resources/net/woggioni/finalguard/test/TestCase8.java @@ -0,0 +1,13 @@ +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class TestCase8 { + + public void testMethod() throws IOException { + try(InputStream is = Files.newInputStream(Path.of("some-path"))) { // Error: is could be final + + } + } +} \ No newline at end of file diff --git a/finalguard/src/main/java/net/woggioni/gradle/finalguard/FinalGuardExtension.java b/finalguard/src/main/java/net/woggioni/gradle/finalguard/FinalGuardExtension.java new file mode 100644 index 0000000..54903a2 --- /dev/null +++ b/finalguard/src/main/java/net/woggioni/gradle/finalguard/FinalGuardExtension.java @@ -0,0 +1,10 @@ +package net.woggioni.gradle.finalguard; + +import org.gradle.api.provider.Property; + +import javax.tools.Diagnostic; + + +public interface FinalGuardExtension { + Property getDiagnosticKind(); +} diff --git a/finalguard/src/main/java/net/woggioni/gradle/finalguard/FinalGuardPlugin.java b/finalguard/src/main/java/net/woggioni/gradle/finalguard/FinalGuardPlugin.java new file mode 100644 index 0000000..dadab5b --- /dev/null +++ b/finalguard/src/main/java/net/woggioni/gradle/finalguard/FinalGuardPlugin.java @@ -0,0 +1,61 @@ +package net.woggioni.gradle.finalguard; + +import lombok.SneakyThrows; +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencySet; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.ExtensionContainer; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.compile.CompileOptions; +import org.gradle.api.tasks.compile.JavaCompile; + +import javax.tools.Diagnostic; +import java.net.URL; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +public class FinalGuardPlugin implements Plugin { + private static final String FINALGUARD_PLUGIN_CONFIGURATION = "finalguard_plugin"; + + @Override + public void apply(final Project project) { + final ExtensionContainer extensionContainer = project.getExtensions(); + final TaskContainer tasks = project.getTasks(); + final ObjectFactory objects = project.getObjects(); + + Configuration javacPluginConfiguration = project.getConfigurations().create(FINALGUARD_PLUGIN_CONFIGURATION); + javacPluginConfiguration.withDependencies(new Action() { + @Override + @SneakyThrows + public void execute(DependencySet dependencies) { + final Class cls = getClass(); + final String resourceName = cls.getName().replace('.', '/') + ".class"; + final URL classUrl = cls.getClassLoader().getResource(resourceName); + if (classUrl.getProtocol().startsWith("jar")) { + final String path = classUrl.toString(); + String manifestPath = path.substring(0, path.lastIndexOf("!") + 1) + + "/META-INF/MANIFEST.MF"; + final Manifest manifest = new Manifest(new URL(manifestPath).openStream()); + final Attributes attr = manifest.getMainAttributes(); + final String version = attr.getValue(Attributes.Name.SPECIFICATION_VERSION); + dependencies.add(project.getDependencies().create("net.woggioni.finalguard:finalguard-javac-plugin:" + version)); + } + } + }); + + final FinalGuardExtension finalGuardExtension = objects.newInstance(FinalGuardExtension.class); + extensionContainer.add("finalGuard", finalGuardExtension); + tasks.withType(JavaCompile.class, javaCompileTask -> { + javaCompileTask.doFirst(t -> { + final Diagnostic.Kind diagnosticKind = finalGuardExtension.getDiagnosticKind().getOrElse(Diagnostic.Kind.WARNING); + final CompileOptions options = javaCompileTask.getOptions(); + options.setAnnotationProcessorPath(options.getAnnotationProcessorPath().plus(javacPluginConfiguration)); + options.getForkOptions().getJvmArgs().add("-Dnet.woggioni.finalguard.diagnostic.level=" + diagnosticKind); + options.getCompilerArgs().add("-Xplugin:net.woggioni.finalguard.FinalGuardPlugin"); + }); + }); + } +} diff --git a/gradle.properties b/gradle.properties index 053426b..d294032 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -lys.catalog.version=2025.02.05 -version.myGradlePlugins=2025.04.16 +lys.catalog.version=2025.09.30 +version.myGradlePlugins=2025.11.17 version.gradle=8.12 gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven diff --git a/settings.gradle b/settings.gradle index 2a5275d..9423271 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,3 +28,5 @@ include 'wildfly' include 'sambal' include 'graalvm' include 'jdeps' +include 'finalguard' +include 'finalguard:finalguard-javac-plugin'