/* * Copyright 2023-2024 the original author or authors. * * Licensed 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 * * https://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 rife.bld.extension; import rife.bld.BaseProject; import rife.bld.extension.checkstyle.OutputFormat; import rife.bld.operations.AbstractProcessOperation; import rife.bld.operations.exceptions.ExitStatusException; import java.io.File; import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; /** * Static code analysis using Checkstyle. * * @author Erik C. Thauvin * @since 1.0 */ public class CheckstyleOperation extends AbstractProcessOperation { private static final Logger LOGGER = Logger.getLogger(CheckstyleOperation.class.getName()); private final Collection excludeRegex_ = new ArrayList<>(); private final Collection exclude_ = new ArrayList<>(); private final Map options_ = new ConcurrentHashMap<>(); private final Set sourceDir_ = new TreeSet<>(); private BaseProject project_; /** * Shows Abstract Syntax Tree(AST) branches that match given XPath query. * * @param xPathQuery the xPath query * @return the checkstyle operation */ public CheckstyleOperation branchMatchingXpath(String xPathQuery) { if (isNotBlank(xPathQuery)) { options_.put("-b", xPathQuery); } return this; } /** * Specifies the location of the file that defines the configuration modules. The location can either be a * filesystem location, or a name passed to the {@link ClassLoader#getResource(String) ClassLoader.getResource() } * method. A configuration file is required. * * @param file the file * @return the checkstyle operation */ public CheckstyleOperation configurationFile(String file) { if (isNotBlank(file)) { options_.put("-c", file); } return this; } /** * Specifies the location of the file that defines the configuration modules. The location can either be a * filesystem location, or a name passed to the {@link ClassLoader#getResource(String) ClassLoader.getResource() } * method. A configuration file is required. * * @param file the file * @return the checkstyle operation */ public CheckstyleOperation configurationFile(File file) { return configurationFile(file.getAbsolutePath()); } /** * Prints all debug logging of CheckStyle utility. * * @param isDebug {@code true} or {@code false} * @return the checkstyle operation */ public CheckstyleOperation debug(boolean isDebug) { if (isDebug) { options_.put("-d", ""); } else { options_.remove("-d"); } return this; } /** * Directory/file to exclude from CheckStyle. The path can be the full, absolute path, or relative to the current * path. Multiple excludes are allowed. * * @param path one or more paths * @return the checkstyle operation * @see #sourceDir(Collection) */ public CheckstyleOperation exclude(String... path) { exclude_.addAll(Arrays.stream(path).map(File::new).toList()); return this; } /** * Directory/file to exclude from CheckStyle. The path can be the full, absolute path, or relative to the current * path. Multiple excludes are allowed. * * @param path one or more paths * @return the checkstyle operation * @see #sourceDir(Collection) */ public CheckstyleOperation exclude(File... path) { exclude_.addAll(List.of(path)); return this; } /** * Directory/file to exclude from CheckStyle. The path can be the full, absolute path, or relative to the current * path. Multiple excludes are allowed. * * @param paths the paths * @return the checkstyle operation * @see #exclude(String...) */ public CheckstyleOperation exclude(Collection paths) { exclude_.addAll(paths); return this; } /** * Directory/file pattern to exclude from CheckStyle. Multiple exclude are allowed. * * @param regex the pattern to exclude * @return the checkstyle operation * @see #excludeRegex(Collection) */ public CheckstyleOperation excludeRegex(String... regex) { excludeRegex_.addAll(List.of(regex)); return this; } /** * Directory/file pattern to exclude from CheckStyle. Multiple exclude are allowed. * * @param regex the patterns to exclude * @return the checkstyle operation * @see #excludeRegex(String...) */ public CheckstyleOperation excludeRegex(Collection regex) { excludeRegex_.addAll(regex); return this; } @Override public void execute() throws IOException, InterruptedException, ExitStatusException { if (project_ == null) { if (LOGGER.isLoggable(Level.SEVERE) && !silent()) { LOGGER.severe("A project must be specified."); } throw new ExitStatusException(ExitStatusException.EXIT_FAILURE); } else { super.execute(); } } /** * Part of the {@link #execute} operation, constructs the command list * to use for building the process. */ @Override protected List executeConstructProcessCommandList() { final List args = new ArrayList<>(); if (project_ != null) { if (sourceDir_.isEmpty()) { sourceDir_.add(project_.srcMainJavaDirectory()); sourceDir_.add(project_.srcTestJavaDirectory()); } args.add(javaTool()); args.add("-cp"); args.add(String.format("%s:%s:%s:%s", new File(project_.libTestDirectory(), "*"), new File(project_.libCompileDirectory(), "*"), project_.buildMainDirectory(), project_.buildTestDirectory())); args.add("com.puppycrawl.tools.checkstyle.Main"); options_.forEach((k, v) -> { args.add(k); if (!v.isEmpty()) { args.add(v); } }); if (!exclude_.isEmpty()) { for (var e : exclude_) { if (e.exists()) { args.add("-e " + e.getAbsolutePath()); } } } if (!excludeRegex_.isEmpty()) { for (var e : excludeRegex_) { if (isNotBlank(e)) { args.add("-x " + e); } } } args.addAll(sourceDir_.stream().map(File::getAbsolutePath).toList()); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, String.join(" ", args)); } } return args; } /** * Configures the {@link BaseProject}. */ @Override public CheckstyleOperation fromProject(BaseProject project) { project_ = project; return this; } /** * Allows ignored modules to be run. * * @param isAllowIgnoreModules {@code true} or {@code false} * @return the checkstyle operation */ public CheckstyleOperation executeIgnoredModules(boolean isAllowIgnoreModules) { if (isAllowIgnoreModules) { options_.put("-E", ""); } else { options_.remove("-E"); } return this; } /** * Specifies the output format. Valid values: {@link OutputFormat#XML}, * {@link OutputFormat#SARIF}, {@link OutputFormat#PLAIN} for the XML, sarif and default logger * respectively. *

* Defaults to {@link OutputFormat#PLAIN}. * * @param format the output format * @return the checkstyle operation */ public CheckstyleOperation format(OutputFormat format) { options_.put("-f", format.label.toLowerCase()); return this; } /** * Generates to output a suppression xml to use to suppress all violations from user's config. Instead of printing * every violation, all violations will be caught and single suppressions xml file will be printed out. * Used only with the {@link #configurationFile(String) configurationFile} option. Output location can be specified * with the {@link #outputPath(String) output} option. * * @param xPathSuppression {@code true} or {@code false} * @return the checkstyle operation */ public CheckstyleOperation generateXpathSuppression(boolean xPathSuppression) { if (xPathSuppression) { options_.put("-g", ""); } else { options_.remove("-g"); } return this; } /* * Determines if a string is not blank. */ private boolean isNotBlank(String s) { return s != null && !s.isBlank(); } /** * This option is used to print the Parse Tree of the Javadoc comment. The file has to contain only Javadoc comment * content excluding '/**' and '*/' at the beginning and at the end respectively. It can only be used on a * single file and cannot be combined with other options. * * @param isTree {@code true} or {@code false} * @return the checkstyle operation */ public CheckstyleOperation javadocTree(boolean isTree) { if (isTree) { options_.put("-j", ""); } else { options_.remove("-j"); } return this; } /** * Returns the command line options. * * @return the command line options */ public Map options() { return options_; } /** * Sets the output file. *

* Defaults to stdout. * * @param file the output file * @return the checkstyle operation */ public CheckstyleOperation outputPath(String file) { if (isNotBlank(file)) { options_.put("-o", file); } return this; } /** * Sets the output file. *

* Defaults to stdout. * * @param file the output file * @return the checkstyle operation */ public CheckstyleOperation outputPath(File file) { return outputPath(file.getAbsolutePath()); } /** * Sets the property files to load. * * @param file the file * @return the checkstyle operation */ public CheckstyleOperation propertiesFile(String file) { if (isNotBlank(file)) { options_.put("-p", file); } return this; } /** * Sets the property files to load. * * @param file the file * @return the checkstyle operation */ public CheckstyleOperation propertiesFile(File file) { return propertiesFile(file.getAbsolutePath()); } /** * Specified the file(s) or folder(s) containing the source files to check. * * @param dir one or more directories * @return the checkstyle operation * @see #sourceDir(Collection) */ public CheckstyleOperation sourceDir(String... dir) { sourceDir_.addAll(Arrays.stream(dir).map(File::new).toList()); return this; } /** * Specified the file(s) or folder(s) containing the source files to check. * * @param dir one or more directories * @return the checkstyle operation * @see #sourceDir(Collection) */ public CheckstyleOperation sourceDir(File... dir) { sourceDir_.addAll(List.of(dir)); return this; } /** * Specified the file(s) or folder(s) containing the source files to check. * * @param dirs the directories * @return the checkstyle operation * @see #sourceDir(String...) */ public CheckstyleOperation sourceDir(Collection dirs) { sourceDir_.addAll(dirs); return this; } /** * Returns the file(s) or folders(s) containing the sources files to check * * @return the files or directories */ public Set sourceDir() { return sourceDir_; } /** * Prints xpath suppressions at the file's line and column position. Argument is the line and column number * (separated by a {@code :} ) in the file that the suppression should be generated for. The option cannot be * used with other options and requires exactly one file to run on to be specified. *

* Note that the generated result will have few queries, joined by pipe({@code |}). Together they will match all * AST nodes on specified line and column. You need to choose only one and recheck that it works. Usage of all of * them is also ok, but might result in undesirable matching and suppress other issues. * * @param lineColumnNumber the line column number * @return the checkstyle operation */ public CheckstyleOperation suppressionLineColumnNumber(String lineColumnNumber) { if (isNotBlank(lineColumnNumber)) { options_.put("-s", lineColumnNumber); } return this; } /** * Sets the length of the tab character. Used only with the * {@link #suppressionLineColumnNumber(String) suppressionLineColumnNumber} option. *

* Default value is {@code 8}. * * @param length the length * @return the checkstyle operation */ public CheckstyleOperation tabWith(int length) { options_.put("-w", String.valueOf(length)); return this; } /** * This option is used to display the Abstract Syntax Tree (AST) without any comments of the specified file. It can * only be used on a single file and cannot be combined with other options. * * @param isTree {@code true} or {@code false} * @return the checkstyle operation */ public CheckstyleOperation tree(boolean isTree) { if (isTree) { options_.put("-t", ""); } else { options_.remove("-t"); } return this; } /** * This option is used to display the Abstract Syntax Tree (AST) with comment nodes excluding Javadoc of the * specified file. It can only be used on a single file and cannot be combined with other options. * * @param isTree {@code true} or {@code false} * @return the checkstyle operation */ public CheckstyleOperation treeWithComments(boolean isTree) { if (isTree) { options_.put("-T", ""); } else { options_.remove("-T"); } return this; } /** * This option is used to display the Abstract Syntax Tree (AST) with Javadoc nodes of the specified file. It can * only be used on a single file and cannot be combined with other options. * * @param isTree {@code true} or {@code false} * @return the checkstyle operation */ public CheckstyleOperation treeWithJavadoc(boolean isTree) { if (isTree) { options_.put("-J", ""); } else { options_.remove("-J"); } return this; } }