package com.sleekbyte.tailor.utils; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.sleekbyte.tailor.common.ColorSettings; import com.sleekbyte.tailor.common.ConstructLengths; import com.sleekbyte.tailor.common.Messages; import com.sleekbyte.tailor.common.Rules; import com.sleekbyte.tailor.common.Severity; import com.sleekbyte.tailor.common.YamlConfiguration; import com.sleekbyte.tailor.format.Format; import com.sleekbyte.tailor.format.Format.IllegalFormatException; import com.sleekbyte.tailor.format.Formatter; import com.sleekbyte.tailor.utils.CLIArgumentParser.CLIArgumentParserException; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.ParseException; import org.yaml.snakeyaml.error.YAMLException; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; /** * Adaptor class for YamlConfiguration and CLIArgumentParser. */ public final class Configuration { private static final String CONFIG_JSON = "../config.json"; private static final Path CODE_CLIMATE_CONFIG = Paths.get(CONFIG_JSON); private static CLIArgumentParser CLIArgumentParser = new CLIArgumentParser(); private Optional<YamlConfiguration> yamlConfiguration; private CommandLine cmd; public Configuration(String[] args) throws ParseException, IOException { cmd = CLIArgumentParser.parseCommandLine(args); yamlConfiguration = YamlConfigurationFileManager.getConfiguration(CLIArgumentParser.getConfigFilePath()); } public boolean shouldPrintHelp() { return CLIArgumentParser.shouldPrintHelp(); } public boolean shouldPrintVersion() { return CLIArgumentParser.shouldPrintVersion(); } public boolean shouldPrintRules() { return CLIArgumentParser.shouldPrintRules(); } public boolean shouldListFiles() { return CLIArgumentParser.shouldListFiles(); } public boolean shouldClearDFAs() { return CLIArgumentParser.shouldClearDFAs() || (yamlConfiguration.isPresent() && yamlConfiguration.get().isPurgeSet()); } /** * Returns number of files specified by the user for the "purge" option. * * @return number specified for the "purge" option */ public int numberOfFilesBeforePurge() throws CLIArgumentParserException { if (CLIArgumentParser.shouldClearDFAs()) { int purge = CLIArgumentParser.numberOfFilesBeforePurge(); if (purge >= 1) { return purge; } else { throw new CLIArgumentParserException("Invalid number of files specified for purge."); } } if (!yamlConfiguration.isPresent() || !yamlConfiguration.get().isPurgeSet()) { // Purge is not set in CLI or config return 0; } int purge = yamlConfiguration.get().getPurge(); if (purge >= 1) { return purge; } else { throw new YAMLException("Invalid number of files specified for purge in config file."); } } /** * Determine if the output should be colorized. * * @return whether the output should be colorized */ public boolean shouldColorOutput() { boolean shouldColorOutput = CLIArgumentParser.shouldColorOutput(); if (shouldColorOutput && yamlConfiguration.isPresent() && !yamlConfiguration.get().getColor().isEmpty()) { String option = yamlConfiguration.get().getColor(); validateColorOption(option); if (option.equals(Messages.DISABLE)) { shouldColorOutput = false; } } return shouldColorOutput; } /** * Determine if the output color should be inverted. * * @return whether the output color should be inverted */ public boolean shouldInvertColorOutput() { boolean shouldInvertColorOutput = CLIArgumentParser.shouldInvertColorOutput(); if (!shouldInvertColorOutput && yamlConfiguration.isPresent() && !yamlConfiguration.get().getColor().isEmpty()) { String option = yamlConfiguration.get().getColor(); validateColorOption(option); if (option.equals(Messages.INVERT)) { shouldInvertColorOutput = true; } } return shouldInvertColorOutput; } /** * Determine is the debug flag is set. * * @return whether the debug flag is set * @throws CLIArgumentParserException if CLI argument parsing for debug fails */ public boolean debugFlagSet() throws CLIArgumentParserException { boolean debugFlagSet = CLIArgumentParser.debugFlagSet(); if (!debugFlagSet && yamlConfiguration.isPresent()) { debugFlagSet = yamlConfiguration.get().isDebug(); } return debugFlagSet; } /** * Collects all rules enabled by default and then filters out rules according to CLI options * and YamlConfiguration file. * * @return list of enabled rules after filtering * @throws CLIArgumentParserException if rule names specified in command line options are not valid */ public Set<Rules> getEnabledRules() throws CLIArgumentParserException { // --only is given precedence over --except // CLI input is given precedence over YAML configuration file // Retrieve included or excluded rules from CLI Set<String> onlySpecificRules = CLIArgumentParser.getOnlySpecificRules(); if (onlySpecificRules.size() > 0) { return getRulesFilteredByOnly(onlySpecificRules); } Set<String> excludedRules = CLIArgumentParser.getExcludedRules(); if (excludedRules.size() > 0) { return getRulesFilteredByExcept(excludedRules); } // Retrieve included or excluded rules from YAML configuration if (yamlConfiguration.isPresent()) { YamlConfiguration configuration = yamlConfiguration.get(); onlySpecificRules = configuration.getOnly(); if (onlySpecificRules.size() > 0) { return getRulesFilteredByOnly(onlySpecificRules); } excludedRules = configuration.getExcept(); if (excludedRules.size() > 0) { return getRulesFilteredByExcept(excludedRules); } } // If `only`/`except` options aren't used then enable all rules return new HashSet<>(Arrays.asList(Rules.values())); } /** * Iterate through pathNames and derive swift source files from each path. * * @return Swift file names * @throws IOException if path specified does not exist */ public Set<String> getFilesToAnalyze() throws IOException, CLIArgumentParserException { Optional<String> srcRoot = getSrcRoot(); List<String> pathNames = new ArrayList<>(); String[] cliPaths = cmd.getArgs(); if (cliPaths.length >= 1) { pathNames.addAll(Arrays.asList(cliPaths)); } Set<String> fileNames = new TreeSet<>(); if (pathNames.size() >= 1) { fileNames.addAll(findFilesInPaths(pathNames)); } else if (Files.isReadable(CODE_CLIMATE_CONFIG)) { pathNames.addAll(getCodeClimateIncludePaths()); fileNames.addAll(findFilesInPaths(pathNames)); } else if (yamlConfiguration.isPresent()) { YamlConfiguration config = yamlConfiguration.get(); Optional<String> configFileLocation = config.getFileLocation(); if (configFileLocation.isPresent() && getFormat() == Format.XCODE) { System.out.println(Messages.TAILOR_CONFIG_LOCATION + configFileLocation.get()); } URI rootUri = new File(srcRoot.orElse(".")).toURI(); Finder finder = new Finder(config.getInclude(), config.getExclude(), rootUri); Files.walkFileTree(Paths.get(rootUri), finder); fileNames.addAll(finder.getFileNames().stream().collect(Collectors.toList())); } else if (srcRoot.isPresent()) { pathNames.add(srcRoot.get()); fileNames.addAll(findFilesInPaths(pathNames)); } return fileNames; } /** * If a Code Climate configuration file exists, then load pathNames from the "include_paths" array. * * @throws IOException if the configuration file cannot be parsed */ private List<String> getCodeClimateIncludePaths() throws IOException { List<String> includePaths = new ArrayList<>(); BufferedReader reader = Files.newBufferedReader(CODE_CLIMATE_CONFIG); try { ConfigJSON config = new Gson().fromJson(reader, ConfigJSON.class); if (config != null && config.include_paths != null) { includePaths = config.include_paths.stream().filter(Objects::nonNull).collect(Collectors.toList()); } } catch (JsonSyntaxException e) { throw new IOException(e.getMessage()); } return includePaths; } /** * Data object to represent a Code Climate configuration, i.e. "config.json". */ private static class ConfigJSON { // Name cannot be camel case because it must match key from Code Climate spec @SuppressWarnings("checkstyle:membername") private List<String> include_paths; } public ConstructLengths parseConstructLengths() throws CLIArgumentParserException { return CLIArgumentParser.parseConstructLengths(); } public Severity getMaxSeverity() throws CLIArgumentParserException { return CLIArgumentParser.getMaxSeverity(); } public String getXcodeprojPath() { return CLIArgumentParser.getXcodeprojPath(); } public static void printHelp() { CLIArgumentParser.printHelp(); } /** * Get an instance of the formatter specified by the user. * @param colorSettings the command-line color settings * @return formatter instance that implements Formatter interface * @throws CLIArgumentParserException if the user-specified format does not correspond to a supported type */ public Formatter getFormatter(ColorSettings colorSettings) throws CLIArgumentParserException, YAMLException { String formatClass = getFormat().getClassName(); Formatter formatter; try { Constructor formatConstructor = Class.forName(formatClass).getConstructor(ColorSettings.class); formatter = (Formatter) formatConstructor.newInstance(colorSettings); } catch (ReflectiveOperationException e) { throw new CLIArgumentParserException("Formatter was not successfully created: " + e); } return formatter; } /** * Retrieves format from CLI or YAML configuration file. * Returns the XCODE format by default if no format is found. * * @return format from CLI or YAML configuration file. XCODE format by default. * @throws CLIArgumentParserException error when parsing the format from CLI arguments * @throws YAMLException error when parsing the format from YAML configuration file */ private Format getFormat() throws CLIArgumentParserException, YAMLException { // Try to get format from CLI/config file. Else use the default format (XCODE) Format format = Format.XCODE; if (CLIArgumentParser.formatOptionSet()) { format = CLIArgumentParser.getFormat(); } else if (yamlConfiguration.isPresent() && !yamlConfiguration.get().getFormat().isEmpty()) { try { format = Format.parseFormat(yamlConfiguration.get().getFormat()); } catch (IllegalFormatException e) { throw new YAMLException(Messages.INVALID_OPTION_VALUE + "format ." + " Options are <" + Format.getFormats() + ">."); } } return format; } private static Set<String> findFilesInPaths(List<String> pathNames) throws IOException { Set<String> fileNames = new HashSet<>(); for (String pathName : pathNames) { File file = new File(pathName); if (file.isDirectory()) { Files.walk(Paths.get(pathName)) .filter(path -> path.toString().endsWith(".swift")) .filter(path -> { File tempFile = path.toFile(); return tempFile.isFile() && tempFile.canRead(); }) .forEach(path -> fileNames.add(path.toString())); } else if (file.isFile() && pathName.endsWith(".swift") && file.canRead()) { fileNames.add(pathName); } } return fileNames; } /** * Checks environment variable SRCROOT (set by Xcode) for the top-level path to the source code and adds path to * pathNames. */ private static Optional<String> getSrcRoot() { String srcRoot = System.getenv("SRCROOT"); if (srcRoot == null || srcRoot.length() == 0) { return Optional.empty(); } return Optional.of(srcRoot); } /** * Checks if rules specified in command line option is valid. * * @param enabledRules all valid rule names * @param specifiedRules rule names specified from command line * @throws CLIArgumentParserException if rule name specified in command line is not valid */ private static void checkValidRules(Set<String> enabledRules, Set<String> specifiedRules) throws CLIArgumentParserException { if (!enabledRules.containsAll(specifiedRules)) { specifiedRules.removeAll(enabledRules); throw new CLIArgumentParserException("The following rules were not recognized: " + specifiedRules); } } private Set<Rules> getRulesFilteredByOnly(Set<String> parsedRules) throws CLIArgumentParserException { Set<Rules> enabledRules = new HashSet<>(Arrays.asList(Rules.values())); Set<String> enabledRuleNames = enabledRules.stream().map(Rules::getName).collect(Collectors.toSet()); Configuration.checkValidRules(enabledRuleNames, parsedRules); return enabledRules.stream().filter(rule -> parsedRules.contains(rule.getName())).collect(Collectors.toSet()); } private Set<Rules> getRulesFilteredByExcept(Set<String> parsedRules) throws CLIArgumentParserException { Set<Rules> enabledRules = new HashSet<>(Arrays.asList(Rules.values())); Set<String> enabledRuleNames = enabledRules.stream().map(Rules::getName).collect(Collectors.toSet()); Configuration.checkValidRules(enabledRuleNames, parsedRules); return enabledRules.stream().filter(rule -> !parsedRules.contains(rule.getName())).collect(Collectors.toSet()); } private void validateColorOption(String colorOption) throws YAMLException { Set<String> colorOptions = new HashSet<>(Arrays.asList(Messages.DISABLE, Messages.INVERT)); if (!colorOptions.contains(colorOption)) { throw new YAMLException("The color option was not recognized. Options are <" + Messages.DISABLE + "|" + Messages.INVERT + ">."); } } }