This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 41a79147f46e6aabc7b0329592e14af247d71eb2
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Sep 30 14:36:50 2023 +0200

    Add the tools used in previous commit for cleaning `@since` and `@version` 
javadoc tags.
---
 .../buildtools/coding/VerifyVersionInJavadoc.java  | 293 +++++++++++++++++++++
 1 file changed, 293 insertions(+)

diff --git 
a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/coding/VerifyVersionInJavadoc.java
 
b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/coding/VerifyVersionInJavadoc.java
new file mode 100644
index 0000000000..90ccbb2ccf
--- /dev/null
+++ 
b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/coding/VerifyVersionInJavadoc.java
@@ -0,0 +1,293 @@
+/*
+ * 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.
+ */
+package org.apache.sis.buildtools.coding;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+
+/**
+ * Verifies the usage of {@code @since} and {@code @version} Javadoc tags in 
source code.
+ * If one of those tags is missing in a public exported class, an error is 
reported.
+ * If those tags are present in a package-private or non-exported class, they 
are removed.
+ *
+ * <h2>How to use</h2>
+ * Run the following command from the project root directory, where "." is the 
current directory
+ * (can be replaced by a path if desired):
+ *
+ * {@snippet lang="shell" :
+ *   java --class-path buildSrc/build/classes/java/main 
org.apache.sis.buildtools.coding.VerifyVersionInJavadoc .
+ *   }
+ *
+ * <h2>Rational</h2>
+ * This tool has been created because in all Apache SIS versions from 0.3 to 
1.3, the {@code @since} and
+ * {@code @version} Javadoc tags were put on all classes, public or not. It 
was a little bit misleading
+ * because non-public classes can be moved, splitted, merged, <i>etc.</i>, 
making the meaning of "since"
+ * confusing. The rule is that {@code @since} should tell when a class was 
first available in public API,
+ * which is not necessarily when it was first created. Finally, with the use 
of JPMS exporting only some
+ * chosen packages, the presence/absence of those tags is a useful way to 
remind whether or not a class
+ * in process of being modified is part of public API.
+ *
+ * <h2>Limitations</h2>
+ * This class does only a gross analysis. It may sometime be wrong.
+ * Developers should check with {@code git diff} before to commit.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+public final class VerifyVersionInJavadoc {
+    /**
+     * Verifies the Javadoc tags in the Java source classes.
+     *
+     * @param  args  the root directory of all branches.
+     * @throws IOException if an error occurred while reading or writing a 
file.
+     */
+    public static void main(String[] args) throws IOException {
+        if (args.length != 1) {
+            System.err.println("Expected: root directory of project to 
filter.");
+            return;
+        }
+        final File root = new File(args[0]);
+        var p = new VerifyVersionInJavadoc();
+        p.listExports(root);
+        p.scan(root);
+    }
+
+    /**
+     * Name of {@value} source files.
+     */
+    private static final String MODULE_INFO = "module-info.java";
+
+    /**
+     * Java keyword to search for.
+     */
+    private static final String EXPORTS = "exports ", PACKAGE = "package ",
+            CLASS = "class ", INTERFACE = "interface ", ENUM = "enum ",
+            PUBLIC = "public ", PROTECTED = "protected ", PRIVATE = "private ";
+
+    /**
+     * All packages that are exported to everyone.
+     */
+    private final Set<String> exportedPackages;
+
+    /**
+     * Whether the file currently being processed is a public or protected 
class, interface or enumeration.
+     */
+    private boolean isPublicType;
+
+    /**
+     * Whether the file currently being processed is an interface or an 
enumeration.
+     * Methods in interfaces are implicitly public, and fields in enumeration 
too.
+     */
+    private boolean isImplicitlyPublic;
+
+    /**
+     * Whether the current file is a test class.
+     */
+    private boolean isTest;
+
+    /**
+     * Creates a new processor.
+     */
+    private VerifyVersionInJavadoc() {
+        exportedPackages = new HashSet<>(64);
+    }
+
+    /**
+     * Finds all exported packages. This method only collect information 
without modifying any file.
+     * It should be invoked before the actual processing done by {@link 
#scan(File)}.
+     *
+     * @param  directory  the directory to scan.
+     * @throws IOException if an error occurred while reading a source file.
+     */
+    private void listExports(final File directory) throws IOException {
+        for (File file : directory.listFiles()) {
+            if (file.getName().equals(MODULE_INFO)) {
+                for (String line : Files.readAllLines(file.toPath())) {
+                    line = getPackageName(EXPORTS, line.trim());
+                    if (line != null && !line.contains(" to ")) {
+                        exportedPackages.add(line);
+                    }
+                }
+                return;     // Do not scan sub-directories.
+            }
+            if (file.isDirectory()) {
+                listExports(file);
+            }
+        }
+    }
+
+    /**
+     * Returns the package name after an {@code exports} or {@code package} 
keyword.
+     *
+     * @param  keyword  {@link #EXPORTS} or {@link #PACKAGE}.
+     * @param  line     the source code line.
+     * @return the package name if found, or {@code null} otherwise.
+     */
+    private static String getPackageName(final String keyword, final String 
line) {
+        if (line.startsWith(keyword) && line.endsWith(";")) {
+            return line.substring(keyword.length(), line.length() - 1).trim();
+        }
+        return null;
+    }
+
+    /**
+     * Returns {@code true} if the source code contains a {@code package} 
statement with the name
+     * of an exported package.
+     *
+     * @param  lines  lines of code to scan.
+     * @return whether the code is for a class or a {@code package-info} in an 
exported package.
+     */
+    private boolean isExportedPackage(final List<String> lines) {
+        return lines.stream().map((line) -> getPackageName(PACKAGE, 
line.trim()))
+                    .filter(Objects::nonNull).findFirst()
+                    .map(exportedPackages::contains).orElse(Boolean.FALSE);
+    }
+
+    /**
+     * Searches for {@code @since} and {@code @version} Javadoc tags in all 
source files.
+     * If the class is not exported or is not public, those tags will be 
removed.
+     * Tags in private methods may also be removed.
+     *
+     * @param  directory  the directory to scan.
+     * @throws IOException if an error occurred while reading or writing a 
source file.
+     */
+    private void scan(final File directory) throws IOException {
+        for (File file : directory.listFiles()) {
+            final String name = file.getName();
+            if (name.endsWith(".java")) {
+                if (!name.equals(MODULE_INFO)) {
+                    process(file.toPath());
+                }
+            } else if (file.isDirectory() && !name.equals("geoapi")) {
+                final boolean old = isTest;
+                isTest |= name.equals("test");
+                scan(file);
+                isTest = old;
+            }
+        }
+    }
+
+    /**
+     * Searches for {@code @since} and {@code @version} Javadoc tags in a 
single source file.
+     * If the class is not exported or not public, those tags will be removed.
+     * Tags in private methods may also be removed.
+     *
+     * @param  file  the file to process.
+     * @throws IOException if an error occurred while reading or writing the 
source file.
+     */
+    private void process(final Path file) throws IOException {
+        final List<String> lines = Files.readAllLines(file, 
StandardCharsets.UTF_8);
+        final boolean isPackageInfo = 
file.getFileName().toString().endsWith("-info.java");
+        final boolean isExportedPackage = isExportedPackage(lines);
+        isPublicType         = false;
+        isImplicitlyPublic   = false;
+        boolean modified     = false;
+        boolean foundSince   = false;
+        boolean foundVersion = false;
+        for (int i=0; i<lines.size(); i++) {
+            final String  line      = lines.get(i).trim();
+            final boolean isSince   = line.startsWith("* @since ");
+            final boolean isVersion = line.startsWith("* @version ");
+            foundSince   |= isSince;
+            foundVersion |= isVersion;
+            if (isSince | isVersion) {
+                if (isTest || !(isExportedPackage && (isPackageInfo || 
isPublic(lines, i)))) {
+                    lines.remove(i--);
+                    modified = true;
+                }
+            } else if (line.equals("*/") && lines.get(i-1).trim().equals("*")) 
{
+                lines.remove(--i);      // Remove trailing empty comment lines.
+                modified = true;
+            }
+        }
+        if (isPublicType & !isTest & !(foundSince & foundVersion)) {
+            System.err.println("Missing @since or @version: " + file);
+        }
+        if (modified) {
+            Files.write(file, lines, StandardCharsets.UTF_8);
+        }
+    }
+
+    /**
+     * Determines whether the class, interface or method after the specified 
line is public or protected.
+     *
+     * @param  lines  all lines of the source file to process.
+     * @param  i      index of current line.
+     */
+    private boolean isPublic(final List<String> lines, int i) {
+        boolean isSkippingComments = true;
+        int innerArguments = 0;
+        while (++i < lines.size()) {
+            String line = lines.get(i).trim();
+            if (isSkippingComments) {
+                if (!line.startsWith("*/")) {
+                    continue;                           // Skip more comment 
lines.
+                }
+                isSkippingComments = false;
+                line = line.substring(2).trim();        // Maybe there is code 
on the same line.
+            }
+            if (line.startsWith("/*") && !line.contains("*/")) {
+                isSkippingComments = true;
+                continue;                               // Comment block 
followed by another comment block.
+            }
+            if (!line.isEmpty() && !line.startsWith("//")) {
+                if (line.contains(PRIVATE)) {
+                    return false;
+                }
+                if (line.contains(PUBLIC) || line.contains(PROTECTED)) {
+                    final boolean implicit = line.contains(INTERFACE) || 
line.contains(ENUM);
+                    if (implicit || line.contains(CLASS)) {
+                        isImplicitlyPublic = implicit;
+                        isPublicType = true;
+                    }
+                    return isPublicType;
+                }
+                final boolean wasInner = (innerArguments != 0);
+                innerArguments += count(line, '(') - count(line, ')');
+                if (innerArguments != 0 || wasInner || line.charAt(0) == '@') 
{         // Skip annotation lines.
+                    continue;
+                }
+                // Not a class or interface, assumes a field or a method.
+                return isPublicType & isImplicitlyPublic;
+            }
+        }
+        return false;       // Unexpected end of file.
+    }
+
+    /**
+     * Returns the number of occurrences of the given character.
+     *
+     * @param  line  the line where to search for the character.
+     * @param  c     the character to count.
+     * @return number of occurrences of the given character.
+     */
+    private static int count(final String line, final char c) {
+        int i=0, n=0;
+        while ((i = line.indexOf(c, i)) >= 0) {
+            i++;
+            n++;
+        }
+        return n;
+    }
+}

Reply via email to