From f05a02eba6890ebb407ac9fb50d078f9c40b3efc Mon Sep 17 00:00:00 2001 From: Alexey Borokhvostov Date: Sun, 29 Sep 2024 20:37:28 +0700 Subject: [PATCH] PHP: Added support for Psalm static analyzer --- php/php.code.analysis/licenseinfo.xml | 1 + .../php/analysis/ImportantFilesImpl.java | 5 +- .../php/analysis/PsalmAnalyzerImpl.java | 265 ++++++++++++ .../modules/php/analysis/PsalmParams.java | 55 +++ .../php/analysis/commands/PHPStan.java | 4 +- .../modules/php/analysis/commands/Psalm.java | 204 +++++++++ .../php/analysis/options/AnalysisOptions.java | 64 +++ .../options/AnalysisOptionsValidator.java | 38 ++ .../options/ValidatorPsalmParameter.java | 80 ++++ ...arser.java => CheckStyleReportParser.java} | 12 +- .../analysis/ui/analyzer/Bundle.properties | 14 + .../ui/analyzer/PsalmCustomizerPanel.form | 222 ++++++++++ .../ui/analyzer/PsalmCustomizerPanel.java | 367 +++++++++++++++++ .../ui/options/AnalysisCategoryPanels.java | 3 +- .../php/analysis/ui/options/Bundle.properties | 16 + .../ui/options/PsalmOptionsPanel.form | 267 ++++++++++++ .../ui/options/PsalmOptionsPanel.java | 387 ++++++++++++++++++ .../php/analysis/ui/resources/psalm.png | Bin 0 -> 267 bytes .../php/analysis/util/AnalysisUiUtils.java | 32 ++ .../psalm/PsalmSupport/src/Calculator.php | 3 + .../PsalmSupport/test/src/CalculatorTest.php | 3 + .../test/unit/data/psalm/nb-php-psalm-log.xml | 123 ++++++ ...t.java => CheckStyleReportParserTest.java} | 44 +- 23 files changed, 2191 insertions(+), 18 deletions(-) create mode 100644 php/php.code.analysis/src/org/netbeans/modules/php/analysis/PsalmAnalyzerImpl.java create mode 100644 php/php.code.analysis/src/org/netbeans/modules/php/analysis/PsalmParams.java create mode 100644 php/php.code.analysis/src/org/netbeans/modules/php/analysis/commands/Psalm.java create mode 100644 php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/ValidatorPsalmParameter.java rename php/php.code.analysis/src/org/netbeans/modules/php/analysis/parsers/{PHPStanReportParser.java => CheckStyleReportParser.java} (94%) create mode 100644 php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PsalmCustomizerPanel.form create mode 100644 php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PsalmCustomizerPanel.java create mode 100644 php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PsalmOptionsPanel.form create mode 100644 php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PsalmOptionsPanel.java create mode 100644 php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/resources/psalm.png create mode 100644 php/php.code.analysis/test/unit/data/psalm/PsalmSupport/src/Calculator.php create mode 100644 php/php.code.analysis/test/unit/data/psalm/PsalmSupport/test/src/CalculatorTest.php create mode 100644 php/php.code.analysis/test/unit/data/psalm/nb-php-psalm-log.xml rename php/php.code.analysis/test/unit/src/org/netbeans/modules/php/analysis/parsers/{PHPStanReportParserTest.java => CheckStyleReportParserTest.java} (71%) diff --git a/php/php.code.analysis/licenseinfo.xml b/php/php.code.analysis/licenseinfo.xml index b7acb6159825..df59f604d060 100644 --- a/php/php.code.analysis/licenseinfo.xml +++ b/php/php.code.analysis/licenseinfo.xml @@ -24,6 +24,7 @@ src/org/netbeans/modules/php/analysis/ui/resources/code-sniffer.png src/org/netbeans/modules/php/analysis/ui/resources/coding-standards-fixer.png src/org/netbeans/modules/php/analysis/ui/resources/mess-detector.png + src/org/netbeans/modules/php/analysis/ui/resources/psalm.png diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ImportantFilesImpl.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ImportantFilesImpl.java index 9b253561bb8c..0b8f0d5ef48f 100644 --- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ImportantFilesImpl.java +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ImportantFilesImpl.java @@ -24,6 +24,7 @@ import org.netbeans.modules.php.analysis.commands.CodeSniffer; import org.netbeans.modules.php.analysis.commands.CodingStandardsFixer; import org.netbeans.modules.php.analysis.commands.PHPStan; +import org.netbeans.modules.php.analysis.commands.Psalm; import org.netbeans.modules.php.spi.phpmodule.ImportantFilesImplementation; import org.netbeans.modules.php.spi.phpmodule.ImportantFilesSupport; import org.netbeans.spi.project.ProjectServiceProvider; @@ -42,7 +43,9 @@ public final class ImportantFilesImpl implements ImportantFilesImplementation { CodingStandardsFixer.DIST_CONFIG_FILE_NAME_V3, PHPStan.CONFIG_FILE_NAME, PHPStan.DIST_CONFIG_FILE_NAME, - PHPStan.ALTERNATIVE_DIST_CONFIG_FILE_NAME}; + PHPStan.ALTERNATIVE_DIST_CONFIG_FILE_NAME, + Psalm.CONFIG_FILE_NAME, + Psalm.DIST_CONFIG_FILE_NAME}; private final ImportantFilesSupport support; diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PsalmAnalyzerImpl.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PsalmAnalyzerImpl.java new file mode 100644 index 000000000000..8c66d70980b8 --- /dev/null +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PsalmAnalyzerImpl.java @@ -0,0 +1,265 @@ +/* + * 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.netbeans.modules.php.analysis; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.Preferences; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.StaticResource; +import org.netbeans.api.fileinfo.NonRecursiveFolder; +import org.netbeans.modules.analysis.spi.Analyzer; +import org.netbeans.modules.php.analysis.commands.Psalm; +import org.netbeans.modules.php.analysis.options.AnalysisOptions; +import org.netbeans.modules.php.analysis.ui.analyzer.PsalmCustomizerPanel; +import org.netbeans.modules.php.analysis.util.AnalysisUtils; +import org.netbeans.modules.php.analysis.util.Mappers; +import org.netbeans.modules.php.api.executable.InvalidPhpExecutableException; +import org.netbeans.modules.php.api.util.StringUtils; +import org.netbeans.modules.refactoring.api.Scope; +import org.netbeans.spi.editor.hints.ErrorDescription; +import org.netbeans.spi.editor.hints.HintsController; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.NbBundle; +import org.openide.util.lookup.ServiceProvider; + +public class PsalmAnalyzerImpl implements Analyzer { + + private static final Logger LOGGER = Logger.getLogger(PsalmAnalyzerImpl.class.getName()); + private final Context context; + private final AtomicBoolean cancelled = new AtomicBoolean(); + + public PsalmAnalyzerImpl(Context context) { + this.context = context; + } + + @NbBundle.Messages({ + "PsalmAnalyzerImpl.psalm.error=Psalm is not valid", + "PsalmAnalyzerImpl.psalm.error.description=Invalid psalm set in IDE Options." + }) + @Override + public Iterable analyze() { + Preferences settings = context.getSettings(); + if (settings != null && !settings.getBoolean(PsalmCustomizerPanel.ENABLED, false)) { + return Collections.emptyList(); + } + + Psalm psalm = getValidPsalm(); + if (psalm == null) { + context.reportAnalysisProblem( + Bundle.PsalmAnalyzerImpl_psalm_error(), + Bundle.PsalmAnalyzerImpl_psalm_error_description()); + return Collections.emptyList(); + } + + PsalmParams psalmParams = new PsalmParams() + .setLevel(getValidPsalmLevel()) + .setConfiguration(getValidPsalmConfiguration()) + .setMemoryLimit(getValidPsalmMemoryLimit()); + Scope scope = context.getScope(); + + Map fileCount = AnalysisUtils.countPhpFiles(scope); + int totalCount = 0; + for (Integer count : fileCount.values()) { + totalCount += count; + } + + context.start(totalCount); + try { + return doAnalyze(scope, psalm, psalmParams, fileCount); + } finally { + context.finish(); + } + } + + @Override + public boolean cancel() { + cancelled.set(true); + return true; + } + + @NbBundle.Messages({ + "PsalmAnalyzerImpl.analyze.error=Psalm analysis error", + "PsalmAnalyzerImpl.analyze.error.description=Error occurred during psalm analysis, review Output window for more information." + }) + private Iterable doAnalyze(Scope scope, Psalm psalm, + PsalmParams params, Map fileCount) { + List errors = new ArrayList<>(); + int progress = 0; + psalm.startAnalyzeGroup(); + for (FileObject root : scope.getSourceRoots()) { + if (cancelled.get()) { + return Collections.emptyList(); + } + List results = psalm.analyze(params, root); + if (results == null) { + context.reportAnalysisProblem( + Bundle.PsalmAnalyzerImpl_analyze_error(), + Bundle.PsalmAnalyzerImpl_analyze_error_description()); + return Collections.emptyList(); + } + errors.addAll(Mappers.map(results)); + progress += fileCount.get(root); + context.progress(progress); + } + + for (FileObject file : scope.getFiles()) { + if (cancelled.get()) { + return Collections.emptyList(); + } + List results = psalm.analyze(params, file); + if (results == null) { + context.reportAnalysisProblem( + Bundle.PsalmAnalyzerImpl_analyze_error(), + Bundle.PsalmAnalyzerImpl_analyze_error_description()); + return Collections.emptyList(); + } + errors.addAll(Mappers.map(results)); + progress += fileCount.get(file); + context.progress(progress); + } + + for (NonRecursiveFolder nonRecursiveFolder : scope.getFolders()) { + if (cancelled.get()) { + return Collections.emptyList(); + } + FileObject folder = nonRecursiveFolder.getFolder(); + List results = psalm.analyze(params, folder); + if (results == null) { + context.reportAnalysisProblem( + Bundle.PsalmAnalyzerImpl_analyze_error(), + Bundle.PsalmAnalyzerImpl_analyze_error_description()); + return Collections.emptyList(); + } + errors.addAll(Mappers.map(results)); + progress += fileCount.get(folder); + context.progress(progress); + } + return errors; + } + + @CheckForNull + private Psalm getValidPsalm() { + String customizerPsalmPath = null; + Preferences settings = context.getSettings(); + if (settings != null) { + customizerPsalmPath = settings.get(PsalmCustomizerPanel.PATH, null); + } + try { + if (StringUtils.hasText(customizerPsalmPath)) { + return Psalm.getCustom(customizerPsalmPath); + } + return Psalm.getDefault(); + } catch (InvalidPhpExecutableException ex) { + LOGGER.log(Level.INFO, null, ex); + } + return null; + } + + private String getValidPsalmLevel() { + String psalmLevel = null; + Preferences settings = context.getSettings(); + if (settings != null) { + psalmLevel = settings.get(PsalmCustomizerPanel.LEVEL, null); + } + if (psalmLevel == null) { + psalmLevel = AnalysisOptions.getInstance().getPsalmLevel(); + } + assert psalmLevel != null; + return AnalysisOptions.getValidPsalmLevel(psalmLevel); + } + + @CheckForNull + private FileObject getValidPsalmConfiguration() { + String psalmConfiguration = null; + Preferences settings = context.getSettings(); + if (settings != null) { + psalmConfiguration = settings.get(PsalmCustomizerPanel.CONFIGURATION, null); + } + if (psalmConfiguration == null) { + psalmConfiguration = AnalysisOptions.getInstance().getPsalmConfigurationPath(); + } + if (StringUtils.isEmpty(psalmConfiguration)) { + return null; + } + return FileUtil.toFileObject(new File(psalmConfiguration)); + } + + private String getValidPsalmMemoryLimit() { + String memoryLimit; + Preferences settings = context.getSettings(); + if (settings != null) { + memoryLimit = settings.get(PsalmCustomizerPanel.MEMORY_LIMIT, ""); // NOI18N + } else { + memoryLimit = String.valueOf(AnalysisOptions.getInstance().getPsalmMemoryLimit()); + } + assert memoryLimit != null; + return memoryLimit; + } + + //~ Inner class + @ServiceProvider(service = AnalyzerFactory.class) + public static final class PsalmAnalyzerFactory extends AnalyzerFactory { + + @StaticResource + private static final String ICON_PATH = "org/netbeans/modules/php/analysis/ui/resources/psalm.png"; // NOI18N + + @NbBundle.Messages("PsalmAnalyzerFactory.displayName=Psalm") + public PsalmAnalyzerFactory() { + super("Psalm", Bundle.PsalmAnalyzerFactory_displayName(), ICON_PATH); + } + + @Override + public Iterable getWarnings() { + return Collections.emptyList(); + } + + @Override + public CustomizerProvider getCustomizerProvider() { + return new CustomizerProvider() { + @Override + public Void initialize() { + return null; + } + + @Override + public PsalmCustomizerPanel createComponent(CustomizerContext context) { + return new PsalmCustomizerPanel(context); + } + }; + } + + @Override + public Analyzer createAnalyzer(Context context) { + return new PsalmAnalyzerImpl(context); + } + + @Override + public void warningOpened(ErrorDescription warning) { + HintsController.setErrors(warning.getFile(), "psalmWarning", Collections.singleton(warning)); // NOI18N + } + } +} diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PsalmParams.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PsalmParams.java new file mode 100644 index 000000000000..6d51ce2e7230 --- /dev/null +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PsalmParams.java @@ -0,0 +1,55 @@ +/* + * 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.netbeans.modules.php.analysis; + +import org.openide.filesystems.FileObject; + +public final class PsalmParams { + + private String level; + private FileObject configuration; + private String memoryLimit; + + public String getLevel() { + return level; + } + + public FileObject getConfiguration() { + return configuration; + } + + public PsalmParams setLevel(String level) { + this.level = level; + return this; + } + + public PsalmParams setConfiguration(FileObject configuration) { + this.configuration = configuration; + return this; + } + + public String getMemoryLimit() { + return memoryLimit; + } + + public PsalmParams setMemoryLimit(String memoryLimit) { + this.memoryLimit = memoryLimit; + return this; + } +} diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/commands/PHPStan.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/commands/PHPStan.java index a3a11635f500..1781080fa916 100644 --- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/commands/PHPStan.java +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/commands/PHPStan.java @@ -34,7 +34,7 @@ import org.netbeans.api.project.Project; import org.netbeans.modules.php.analysis.PHPStanParams; import org.netbeans.modules.php.analysis.options.AnalysisOptions; -import org.netbeans.modules.php.analysis.parsers.PHPStanReportParser; +import org.netbeans.modules.php.analysis.parsers.CheckStyleReportParser; import org.netbeans.modules.php.analysis.results.Result; import org.netbeans.modules.php.analysis.ui.options.AnalysisOptionsPanelController; import org.netbeans.modules.php.api.executable.InvalidPhpExecutableException; @@ -124,7 +124,7 @@ public List analyze(PHPStanParams params, FileObject file) { return null; } - return PHPStanReportParser.parse(XML_LOG, file, workDir); + return CheckStyleReportParser.parse(XML_LOG, file, workDir); } catch (CancellationException ex) { // cancelled return Collections.emptyList(); diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/commands/Psalm.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/commands/Psalm.java new file mode 100644 index 000000000000..dd1d3666f6eb --- /dev/null +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/commands/Psalm.java @@ -0,0 +1,204 @@ +/* + * 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.netbeans.modules.php.analysis.commands; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.api.extexecution.ExecutionDescriptor; +import org.netbeans.api.project.FileOwnerQuery; +import org.netbeans.api.project.Project; +import org.netbeans.modules.php.analysis.PsalmParams; +import org.netbeans.modules.php.analysis.options.AnalysisOptions; +import org.netbeans.modules.php.analysis.parsers.CheckStyleReportParser; +import org.netbeans.modules.php.analysis.results.Result; +import org.netbeans.modules.php.analysis.ui.options.AnalysisOptionsPanelController; +import org.netbeans.modules.php.api.executable.InvalidPhpExecutableException; +import org.netbeans.modules.php.api.executable.PhpExecutable; +import org.netbeans.modules.php.api.executable.PhpExecutableValidator; +import org.netbeans.modules.php.api.util.StringUtils; +import org.netbeans.modules.php.api.util.UiUtils; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.NbBundle; + +public final class Psalm { + + public static final String NAME = "psalm"; // NOI18N + public static final String LONG_NAME = NAME + ".phar"; // NOI18N + static final File XML_LOG = new File(System.getProperty("java.io.tmpdir"), "nb-php-psalm-log.xml"); // NOI18N + private static final Logger LOGGER = Logger.getLogger(Psalm.class.getName()); + + // params + private static final String CONFIGURATION_PARAM = "--config=%s"; // NOI18N + private static final String LEVEL_PARAM = "--error-level=%s"; // NOI18N + private static final String MEMORY_LIMIT_PARAM = "--memory-limit=%s"; // NOI18N + private static final String ERROR_FORMAT_PARAM = "--output-format=checkstyle"; // NOI18N + private static final String NO_PROGRESS_PARAM = "--no-progress"; // NOI18N + private static final String NO_CACHE_PARAM = "--no-cache"; // NOI18N + private static final List ANALYZE_DEFAULT_PARAMS = Arrays.asList( + NO_PROGRESS_PARAM, + NO_CACHE_PARAM, + ERROR_FORMAT_PARAM + ); + + // configuration files + public static final String CONFIG_FILE_NAME = "psalm.xml"; // NOI18N + public static final String DIST_CONFIG_FILE_NAME = "psalm.xml.dist"; // NOI18N + + private final String psalmPath; + private int analyzeGroupCounter = 1; + + private Psalm(String psalmPath) { + this.psalmPath = psalmPath; + } + + public static Psalm getDefault() throws InvalidPhpExecutableException { + return getCustom(AnalysisOptions.getInstance().getPsalmPath()); + } + + public static Psalm getCustom(String psalmPath) throws InvalidPhpExecutableException { + String error = validate(psalmPath); + if (error != null) { + throw new InvalidPhpExecutableException(error); + } + return new Psalm(psalmPath); + } + + @NbBundle.Messages("Psalm.script.label=Psalm") + public static String validate(String psalmPath) { + return PhpExecutableValidator.validateCommand(psalmPath, Bundle.Psalm_script_label()); + } + + public void startAnalyzeGroup() { + analyzeGroupCounter = 1; + } + + @NbBundle.Messages({ + "# {0} - counter", + "Psalm.analyze=Psalm (analyze #{0})" + }) + @CheckForNull + public List analyze(PsalmParams params, FileObject file) { + assert file.isValid() : "Invalid file given: " + file; + try { + FileObject workDir = findWorkDir(file); + //there is no need to specify a directory for analysis if it is the root directory of the project + boolean fileIsWorkDir = workDir == null ? false : file.getPath().equals(workDir.getPath()); + Integer result = getExecutable(Bundle.Psalm_analyze(analyzeGroupCounter++), workDir == null ? null : FileUtil.toFile(workDir)) + .additionalParameters(getParameters(params, fileIsWorkDir ? null : file)) + .runAndWait(getDescriptor(), "Running psalm..."); // NOI18N + if (result == null) { + return null; + } + + return CheckStyleReportParser.parse(XML_LOG, file, workDir); + } catch (CancellationException ex) { + // cancelled + return Collections.emptyList(); + } catch (ExecutionException ex) { + LOGGER.log(Level.INFO, null, ex); + UiUtils.processExecutionException(ex, AnalysisOptionsPanelController.OPTIONS_SUB_PATH); + } + return null; + } + + /** + * Finds project directory for the given file since it can contain + * {@code psalm.xml}, {@code psalm.xml.dist}. + * + * @param file file to find project directory for + * @return project directory or {@code null} + */ + @CheckForNull + private FileObject findWorkDir(FileObject file) { + assert file != null; + Project project = FileOwnerQuery.getOwner(file); + FileObject workDir = null; + if (project != null) { + workDir = project.getProjectDirectory(); + if (LOGGER.isLoggable(Level.FINE)) { + if (workDir != null) { + LOGGER.log(Level.FINE, "Project directory for {0} is found in {1}", new Object[]{FileUtil.toFile(file), workDir}); // NOI18N + } else { + // the file/directory may not be in a PHP project + LOGGER.log(Level.FINE, "Project directory for {0} is not found", FileUtil.toFile(file)); // NOI18N + } + } + } + return workDir; + } + + private PhpExecutable getExecutable(String title, @NullAllowed File workDir) { + PhpExecutable executable = new PhpExecutable(psalmPath) + .optionsSubcategory(AnalysisOptionsPanelController.OPTIONS_SUB_PATH) + .fileOutput(XML_LOG, "UTF-8", false) // NOI18N + .redirectErrorStream(false) + .displayName(title); + if (workDir != null) { + executable.workDir(workDir); + } + return executable; + } + + private ExecutionDescriptor getDescriptor() { + return PhpExecutable.DEFAULT_EXECUTION_DESCRIPTOR + .optionsPath(AnalysisOptionsPanelController.OPTIONS_PATH) + .frontWindowOnError(false) + .inputVisible(false) + .preExecution(() -> { + if (XML_LOG.isFile()) { + if (!XML_LOG.delete()) { + LOGGER.log(Level.INFO, "Cannot delete log file {0}", XML_LOG.getAbsolutePath()); // NOI18N + } + } + }); + } + + private List getParameters(PsalmParams parameters, FileObject file) { + // /path/to/{dir|file} + List params = new ArrayList<>(); + params.addAll(ANALYZE_DEFAULT_PARAMS); + String level = parameters.getLevel(); + if (!StringUtils.isEmpty(level)) { + params.add(String.format(LEVEL_PARAM, level)); + } + FileObject configuration = parameters.getConfiguration(); + if (configuration != null) { + params.add(String.format(CONFIGURATION_PARAM, FileUtil.toFile(configuration).getAbsolutePath())); + } + String memoryLimit = parameters.getMemoryLimit(); + if (!StringUtils.isEmpty(memoryLimit)) { + params.add(String.format(MEMORY_LIMIT_PARAM, memoryLimit)); + } + if (file != null) { + params.add(FileUtil.toFile(file).getAbsolutePath()); + } + return params; + } + +} diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/AnalysisOptions.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/AnalysisOptions.java index 9f370ccb3444..9ab6a6d6fb3d 100644 --- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/AnalysisOptions.java +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/AnalysisOptions.java @@ -25,6 +25,7 @@ import org.netbeans.modules.php.analysis.commands.CodingStandardsFixer; import org.netbeans.modules.php.analysis.commands.MessDetector; import org.netbeans.modules.php.analysis.commands.PHPStan; +import org.netbeans.modules.php.analysis.commands.Psalm; import org.netbeans.modules.php.analysis.util.AnalysisUtils; import org.netbeans.modules.php.api.util.FileUtils; import org.openide.util.NbPreferences; @@ -58,11 +59,19 @@ public final class AnalysisOptions { private static final String PHPSTAN_MEMORY_LIMIT = "phpstan.memory.limit"; // NOI18N public static final int PHPSTAN_MIN_LEVEL = Integer.getInteger("nb.phpstan.min.level", 0); // NOI18N public static final int PHPSTAN_MAX_LEVEL = Integer.getInteger("nb.phpstan.max.level", 9); // NOI18N + // Psalm - PHP Static Analysis Tool + private static final String PSALM_PATH = "psalm.path"; // NOI18N + private static final String PSALM_LEVEL = "psalm.level"; // NOI18N + private static final String PSALM_CONFIGURATION = "psalm.configuration"; // NOI18N + private static final String PSALM_MEMORY_LIMIT = "psalm.memory.limit"; // NOI18N + public static final int PSALM_MIN_LEVEL = Integer.getInteger("nb.psalm.min.level", 1); // NOI18N + public static final int PSALM_MAX_LEVEL = Integer.getInteger("nb.psalm.max.level", 8); // NOI18N private volatile boolean codeSnifferSearched = false; private volatile boolean messDetectorSearched = false; private volatile boolean codingStandardsFixerSearched = false; private volatile boolean phpstanSearched = false; + private volatile boolean psalmSearched = false; private AnalysisOptions() { } @@ -274,4 +283,59 @@ private Preferences getPreferences() { return NbPreferences.forModule(AnalysisOptions.class).node(PREFERENCES_PATH); } + // psalm + @CheckForNull + public String getPsalmPath() { + String psalmPath = getPreferences().get(PSALM_PATH, null); + if (psalmPath == null && !psalmSearched) { + psalmSearched = true; + List scripts = FileUtils.findFileOnUsersPath(Psalm.NAME, Psalm.LONG_NAME); + if (!scripts.isEmpty()) { + psalmPath = scripts.get(0); + setMessDetectorPath(psalmPath); + } + } + return psalmPath; + } + + public void setPsalmPath(String path) { + getPreferences().put(PSALM_PATH, path); + } + + public String getPsalmLevel() { + String level = getPreferences().get(PSALM_LEVEL, String.valueOf(PSALM_MIN_LEVEL)); + return getValidPsalmLevel(level); + } + + public void setPsalmLevel(String level) { + getPreferences().put(PSALM_LEVEL, getValidPsalmLevel(level)); + } + + public static String getValidPsalmLevel(String level) { + String psalmLevel; + try { + psalmLevel = String.valueOf(AnalysisUtils.getValidInt(PSALM_MIN_LEVEL, PSALM_MAX_LEVEL, Integer.valueOf(level))); + } catch (NumberFormatException e) { + psalmLevel = level; + } + return psalmLevel; + } + + @CheckForNull + public String getPsalmConfigurationPath() { + return getPreferences().get(PSALM_CONFIGURATION, null); + } + + public void setPsalmConfigurationPath(String configuration) { + getPreferences().put(PSALM_CONFIGURATION, configuration); + } + + public String getPsalmMemoryLimit() { + return getPreferences().get(PSALM_MEMORY_LIMIT, ""); // NOI18N + } + + public void setPsalmMemoryLimit(String memoryLimit) { + getPreferences().put(PSALM_MEMORY_LIMIT, memoryLimit); + } + } diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/AnalysisOptionsValidator.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/AnalysisOptionsValidator.java index d29e56279b38..b83bade28b4c 100644 --- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/AnalysisOptionsValidator.java +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/AnalysisOptionsValidator.java @@ -26,6 +26,7 @@ import org.netbeans.modules.php.analysis.commands.CodingStandardsFixer; import org.netbeans.modules.php.analysis.commands.MessDetector; import org.netbeans.modules.php.analysis.commands.PHPStan; +import org.netbeans.modules.php.analysis.commands.Psalm; import org.netbeans.modules.php.api.util.FileUtils; import org.netbeans.modules.php.api.util.StringUtils; import org.netbeans.modules.php.api.validation.ValidationResult; @@ -36,6 +37,7 @@ public final class AnalysisOptionsValidator { private static final Pattern PHPSTAN_MEMORY_LIMIT_PATTERN = Pattern.compile("^\\-?\\d+[kmg]?$", Pattern.CASE_INSENSITIVE); // NOI18N + private static final Pattern PSALM_MEMORY_LIMIT_PATTERN = Pattern.compile("^\\-?\\d+[kmg]?$", Pattern.CASE_INSENSITIVE); // NOI18N private final ValidationResult result = new ValidationResult(); public AnalysisOptionsValidator validateCodeSniffer(ValidatorCodeSnifferParameter param) { @@ -69,6 +71,13 @@ public AnalysisOptionsValidator validatePHPStan(ValidatorPHPStanParameter param) return this; } + public AnalysisOptionsValidator validatePsalm(ValidatorPsalmParameter param) { + validatePsalmPath(param.getPsalmPath()); + validatePsalmConfiguration(param.getConfiguration()); + validatePsalmMemoryLimit(param.getMemoryLimit()); + return this; + } + public ValidationResult getResult() { return result; } @@ -142,4 +151,33 @@ private AnalysisOptionsValidator validatePHPStanMemoryLimit(String memoryLimit) return this; } + private AnalysisOptionsValidator validatePsalmPath(String psalmPath) { + String warning = Psalm.validate(psalmPath); + if (warning != null) { + result.addWarning(new ValidationResult.Message("psalm.path", warning)); // NOI18N + } + return this; + } + + private AnalysisOptionsValidator validatePsalmConfiguration(String configuration) { + if (!StringUtils.isEmpty(configuration)) { + String warning = FileUtils.validateFile("Configuration file", configuration, false); // NOI18N + if (warning != null) { + result.addWarning(new ValidationResult.Message("psalm.configuration", warning)); // NOI18N + } + } + return this; + } + + @NbBundle.Messages("AnalysisOptionsValidator.psalm.memory.limit.invalid=Valid memory limit value must be set.") + private AnalysisOptionsValidator validatePsalmMemoryLimit(String memoryLimit) { + if (!StringUtils.isEmpty(memoryLimit)) { + Matcher matcher = PSALM_MEMORY_LIMIT_PATTERN.matcher(memoryLimit); + if (!matcher.matches()) { + result.addWarning(new ValidationResult.Message("psalm.memory.limit", Bundle.AnalysisOptionsValidator_psalm_memory_limit_invalid())); // NOI18N + } + } + return this; + } + } diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/ValidatorPsalmParameter.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/ValidatorPsalmParameter.java new file mode 100644 index 000000000000..edfee3ff4936 --- /dev/null +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/options/ValidatorPsalmParameter.java @@ -0,0 +1,80 @@ +/* + * 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.netbeans.modules.php.analysis.options; + +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.modules.php.analysis.ui.analyzer.PsalmCustomizerPanel; +import org.netbeans.modules.php.analysis.ui.options.PsalmOptionsPanel; +import org.netbeans.modules.php.api.util.StringUtils; + +public final class ValidatorPsalmParameter { + + @NullAllowed + private final String psalmPath; + @NullAllowed + private final String configuration; + @NullAllowed + private final String memoryLimit; + + public static ValidatorPsalmParameter create(PsalmOptionsPanel panel) { + return new ValidatorPsalmParameter(panel); + } + + public static ValidatorPsalmParameter create(PsalmCustomizerPanel panel) { + return new ValidatorPsalmParameter(panel); + } + + private ValidatorPsalmParameter() { + psalmPath = null; + configuration = null; + memoryLimit = null; + } + + private ValidatorPsalmParameter(PsalmOptionsPanel panel) { + psalmPath = panel.getPsalmPath(); + configuration = panel.getPsalmConfigurationPath(); + memoryLimit = panel.getPsalmMemoryLimit(); + } + + private ValidatorPsalmParameter(PsalmCustomizerPanel panel) { + if (StringUtils.hasText(panel.getPsalmPath())) { + psalmPath = panel.getPsalmPath(); + } else { + psalmPath = AnalysisOptions.getInstance().getPsalmPath(); + } + configuration = panel.getConfiguration(); + memoryLimit = panel.getMemoryLimit(); + } + + @CheckForNull + public String getPsalmPath() { + return psalmPath; + } + + @CheckForNull + public String getConfiguration() { + return configuration; + } + + @CheckForNull + public String getMemoryLimit() { + return memoryLimit; + } +} diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/parsers/PHPStanReportParser.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/parsers/CheckStyleReportParser.java similarity index 94% rename from php/php.code.analysis/src/org/netbeans/modules/php/analysis/parsers/PHPStanReportParser.java rename to php/php.code.analysis/src/org/netbeans/modules/php/analysis/parsers/CheckStyleReportParser.java index 428aab352cb2..913792344447 100644 --- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/parsers/PHPStanReportParser.java +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/parsers/CheckStyleReportParser.java @@ -46,12 +46,12 @@ import org.xml.sax.helpers.DefaultHandler; /** - * Parser for PHPStan xml report file. + * Parser for PHPStan and Psalm xml report files. */ -public class PHPStanReportParser extends DefaultHandler { +public class CheckStyleReportParser extends DefaultHandler { private static final String PHP_EXT = ".php"; // NOI18N - private static final Logger LOGGER = Logger.getLogger(PHPStanReportParser.class.getName()); + private static final Logger LOGGER = Logger.getLogger(CheckStyleReportParser.class.getName()); private final List results = new ArrayList<>(); private final XMLReader xmlReader; @@ -61,14 +61,14 @@ public class PHPStanReportParser extends DefaultHandler { @NullAllowed private final FileObject workDir; - private PHPStanReportParser(FileObject root, @NullAllowed FileObject workDir) throws SAXException { + private CheckStyleReportParser(FileObject root, @NullAllowed FileObject workDir) throws SAXException { this.xmlReader = FileUtils.createXmlReader(); this.root = root; this.workDir = workDir; } - private static PHPStanReportParser create(Reader reader, FileObject root, @NullAllowed FileObject workDir) throws SAXException, IOException { - PHPStanReportParser parser = new PHPStanReportParser(root, workDir); + private static CheckStyleReportParser create(Reader reader, FileObject root, @NullAllowed FileObject workDir) throws SAXException, IOException { + CheckStyleReportParser parser = new CheckStyleReportParser(root, workDir); parser.xmlReader.setContentHandler(parser); parser.xmlReader.parse(new InputSource(reader)); return parser; diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/Bundle.properties b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/Bundle.properties index 7656794c79c5..b6f194185a97 100644 --- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/Bundle.properties +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/Bundle.properties @@ -44,6 +44,8 @@ CodingStandardsFixerCustomizerPanel.searchButton.text=&Search... CodingStandardsFixerCustomizerPanel.codingStandardsFixerTextField.text= CodeSnifferCustomizerPanel.enabledCheckBox.text=&Enabled MessDetectorCustomizerPanel.enabledCheckBox.text=&Enabled + +# phpstan PHPStanCustomizerPanel.phpStanConfigurationBrowseButton.text=&Browse... PHPStanCustomizerPanel.phpStanLevelLabel.text=&Level: PHPStanCustomizerPanel.phpStanConfigurationLabel.text=&Configuration: @@ -53,3 +55,15 @@ PHPStanCustomizerPanel.phpStanLabel.text=&PHPStan: PHPStanCustomizerPanel.phpStanTextField.text= PHPStanCustomizerPanel.phpStanBrowseButton.text=B&rowse... PHPStanCustomizerPanel.phpStanSearchButton.text=&Search... + +# psalm +PsalmCustomizerPanel.psalmEnabledCheckBox.text=&Enabled +PsalmCustomizerPanel.psalmLabel.text=&Psalm: +PsalmCustomizerPanel.psalmLabel.AccessibleContext.accessibleName=&Psalm: +PsalmCustomizerPanel.psalmTextField.text= +PsalmCustomizerPanel.psalmBrowseButton.text=B&rowse... +PsalmCustomizerPanel.psalmSearchButton.text=&Search... +PsalmCustomizerPanel.psalmConfigurationLabel.text=&Configuration: +PsalmCustomizerPanel.psalmConfigurationBrowseButton.text=&Browse... +PsalmCustomizerPanel.psalmMemoryLimitLabel.text=&Memory Limit: +PsalmCustomizerPanel.psalmLevelLabel.text=&Level: diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PsalmCustomizerPanel.form b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PsalmCustomizerPanel.form new file mode 100644 index 000000000000..95f665c30190 --- /dev/null +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PsalmCustomizerPanel.form @@ -0,0 +1,222 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PsalmCustomizerPanel.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PsalmCustomizerPanel.java new file mode 100644 index 000000000000..495e3136e094 --- /dev/null +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PsalmCustomizerPanel.java @@ -0,0 +1,367 @@ +/* + * 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.netbeans.modules.php.analysis.ui.analyzer; + +import java.awt.Component; +import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.concurrent.TimeUnit; +import java.util.prefs.Preferences; +import javax.swing.DefaultComboBoxModel; +import javax.swing.GroupLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.LayoutStyle; +import org.netbeans.modules.analysis.spi.Analyzer; +import org.netbeans.modules.php.analysis.commands.Psalm; +import org.netbeans.modules.php.analysis.options.AnalysisOptions; +import org.netbeans.modules.php.analysis.options.AnalysisOptionsValidator; +import org.netbeans.modules.php.analysis.options.ValidatorPsalmParameter; +import org.netbeans.modules.php.analysis.ui.AnalysisDefaultDocumentListener; +import org.netbeans.modules.php.analysis.util.AnalysisUiUtils; +import org.netbeans.modules.php.api.validation.ValidationResult; +import org.openide.awt.Mnemonics; +import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; + +public class PsalmCustomizerPanel extends JPanel { + + public static final String ENABLED = "psalm.enabled"; // NOI18N + public static final String PATH = "psalm.path"; // NOI18N + public static final String LEVEL = "psalm.level"; // NOI18N + public static final String CONFIGURATION = "psalm.configuration"; // NOI18N + public static final String MEMORY_LIMIT = "psalm.memory.limit"; // NOI18N + private static final RequestProcessor RP = new RequestProcessor(PsalmCustomizerPanel.class); + private static final long serialVersionUID = -3450253368766485405L; + + final Analyzer.CustomizerContext context; + final Preferences settings; + + public PsalmCustomizerPanel(Analyzer.CustomizerContext context) { + assert EventQueue.isDispatchThread(); + assert context != null; + + this.context = context; + this.settings = context.getSettings(); + initComponents(); + init(); + } + + private void init() { + initEnabledCheckBox(); + initPsalmField(); + initLevelComboBox(); + initConfigurationTextField(); + initMemoryLimitTextField(); + // avoid NPE: don't set errors during initializing + RP.schedule(() -> { + EventQueue.invokeLater(() -> { + context.setError(null); + if (psalmEnabledCheckBox.isSelected()) { + validateData(); + } + }); + }, 1000, TimeUnit.MILLISECONDS); + } + + private void initEnabledCheckBox() { + assert EventQueue.isDispatchThread(); + psalmEnabledCheckBox.addItemListener(e -> { + setAllComponetsEnabled(psalmEnabledCheckBox.isSelected()); + setPsalmEnabled(); + }); + boolean isEnabled = settings.getBoolean(ENABLED, false); + psalmEnabledCheckBox.setSelected(isEnabled); + setAllComponetsEnabled(isEnabled); + psalmEnabledCheckBox.addItemListener(e -> { + if (!psalmEnabledCheckBox.isSelected()) { + context.setError(null); + } else { + validateData(); + } + }); + } + + private void initPsalmField() { + assert EventQueue.isDispatchThread(); + psalmTextField.setText(settings.get(PATH, AnalysisOptions.getInstance().getPsalmPath())); + psalmTextField.getDocument().addDocumentListener(new AnalysisDefaultDocumentListener(() -> setPsalmPath())); + } + + private void initLevelComboBox() { + assert EventQueue.isDispatchThread(); + psalmLevelComboBox.removeAllItems(); + // NETBEANS-2974 + // allow empty level option to use a level of a configuration file + psalmLevelComboBox.addItem(""); // NOI18N + for (int i = AnalysisOptions.PSALM_MIN_LEVEL; i <= AnalysisOptions.PSALM_MAX_LEVEL; i++) { + psalmLevelComboBox.addItem(String.valueOf(i)); + } + psalmLevelComboBox.setSelectedItem(getValidLevel()); + psalmLevelComboBox.addItemListener(e -> setLevel()); + } + + private String getValidLevel() { + String level = settings.get(LEVEL, AnalysisOptions.getInstance().getPsalmLevel()); + return AnalysisOptions.getValidPsalmLevel(level); + } + + private void initConfigurationTextField() { + assert EventQueue.isDispatchThread(); + psalmConfigurationTextField.setText(settings.get(CONFIGURATION, AnalysisOptions.getInstance().getPsalmConfigurationPath())); + psalmConfigurationTextField.getDocument().addDocumentListener(new AnalysisDefaultDocumentListener(() -> setConfiguration())); + } + + private void initMemoryLimitTextField() { + assert EventQueue.isDispatchThread(); + psalmMemoryLimitTextField.setText(settings.get(MEMORY_LIMIT, AnalysisOptions.getInstance().getPsalmMemoryLimit())); + psalmMemoryLimitTextField.getDocument().addDocumentListener(new AnalysisDefaultDocumentListener(() -> setMemoryLimit())); + } + + public String getPsalmPath() { + return psalmTextField.getText().trim(); + } + + public String getLevel() { + return (String) psalmLevelComboBox.getSelectedItem(); + } + + public String getConfiguration() { + return psalmConfigurationTextField.getText().trim(); + } + + public String getMemoryLimit() { + return psalmMemoryLimitTextField.getText().trim(); + } + + private void setPsalmEnabled() { + settings.putBoolean(ENABLED, psalmEnabledCheckBox.isSelected()); + } + + private void setPsalmPath() { + if (validateData()) { + settings.put(PATH, getPsalmPath()); + } + } + + private void setLevel() { + settings.put(LEVEL, getLevel()); + } + + private void setConfiguration() { + if (validateData()) { + settings.put(CONFIGURATION, getConfiguration()); + } + } + + private void setMemoryLimit() { + if (validateData()) { + settings.put(MEMORY_LIMIT, getMemoryLimit()); + } + } + + private boolean validateData() { + ValidatorPsalmParameter param = ValidatorPsalmParameter.create(this); + ValidationResult result = new AnalysisOptionsValidator() + .validatePsalm(param) + .getResult(); + if (result.hasErrors()) { + context.setError(result.getErrors().get(0).getMessage()); + return false; + } + if (result.hasWarnings()) { + context.setError(result.getWarnings().get(0).getMessage()); + return false; + } + context.setError(null); + return true; + } + + private void setAllComponetsEnabled(boolean isEnabled) { + Component[] components = getComponents(); + for (Component component : components) { + if (component != psalmEnabledCheckBox) { + component.setEnabled(isEnabled); + } + } + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + psalmEnabledCheckBox = new javax.swing.JCheckBox(); + psalmConfigurationLabel = new javax.swing.JLabel(); + psalmConfigurationTextField = new javax.swing.JTextField(); + psalmConfigurationBrowseButton = new javax.swing.JButton(); + psalmLevelLabel = new javax.swing.JLabel(); + psalmLevelComboBox = new javax.swing.JComboBox(); + psalmMemoryLimitLabel = new javax.swing.JLabel(); + psalmMemoryLimitTextField = new javax.swing.JTextField(); + psalmLabel = new javax.swing.JLabel(); + psalmTextField = new javax.swing.JTextField(); + psalmBrowseButton = new javax.swing.JButton(); + psalmSearchButton = new javax.swing.JButton(); + + org.openide.awt.Mnemonics.setLocalizedText(psalmEnabledCheckBox, org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmEnabledCheckBox.text")); // NOI18N + + psalmConfigurationLabel.setLabelFor(psalmConfigurationTextField); + org.openide.awt.Mnemonics.setLocalizedText(psalmConfigurationLabel, org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmConfigurationLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(psalmConfigurationBrowseButton, org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmConfigurationBrowseButton.text")); // NOI18N + psalmConfigurationBrowseButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + psalmConfigurationBrowseButtonActionPerformed(evt); + } + }); + + psalmLevelLabel.setLabelFor(psalmLevelComboBox); + org.openide.awt.Mnemonics.setLocalizedText(psalmLevelLabel, org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmLevelLabel.text")); // NOI18N + + psalmLevelComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "0", "1", "2", "3", "4", "5", "6", "7" })); + + psalmMemoryLimitLabel.setLabelFor(psalmMemoryLimitTextField); + org.openide.awt.Mnemonics.setLocalizedText(psalmMemoryLimitLabel, org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmMemoryLimitLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(psalmLabel, org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmLabel.text")); // NOI18N + psalmLabel.setRequestFocusEnabled(false); + + psalmTextField.setText(org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmTextField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(psalmBrowseButton, org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmBrowseButton.text")); // NOI18N + psalmBrowseButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + psalmBrowseButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(psalmSearchButton, org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmSearchButton.text")); // NOI18N + psalmSearchButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + psalmSearchButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(psalmEnabledCheckBox) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(psalmLabel, javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(psalmConfigurationLabel) + .addComponent(psalmLevelLabel) + .addComponent(psalmMemoryLimitLabel))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(psalmLevelComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(psalmMemoryLimitTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(psalmConfigurationTextField) + .addComponent(psalmTextField)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(psalmBrowseButton, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(psalmConfigurationBrowseButton, javax.swing.GroupLayout.Alignment.TRAILING)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(psalmSearchButton)))) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(psalmEnabledCheckBox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(psalmLabel) + .addComponent(psalmTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(psalmBrowseButton) + .addComponent(psalmSearchButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(psalmConfigurationLabel) + .addComponent(psalmConfigurationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(psalmConfigurationBrowseButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(psalmLevelLabel) + .addComponent(psalmLevelComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(psalmMemoryLimitLabel) + .addComponent(psalmMemoryLimitTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + ); + + psalmLabel.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(PsalmCustomizerPanel.class, "PsalmCustomizerPanel.psalmLabel.AccessibleContext.accessibleName")); // NOI18N + }// //GEN-END:initComponents + + private void psalmConfigurationBrowseButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_psalmConfigurationBrowseButtonActionPerformed + File file = AnalysisUiUtils.browsePsalmConfiguration(); + if (file != null) { + psalmConfigurationTextField.setText(file.getAbsolutePath()); + } + }//GEN-LAST:event_psalmConfigurationBrowseButtonActionPerformed + + private void psalmBrowseButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_psalmBrowseButtonActionPerformed + File file = AnalysisUiUtils.browsePsalm(); + if (file != null) { + psalmTextField.setText(file.getAbsolutePath()); + } + }//GEN-LAST:event_psalmBrowseButtonActionPerformed + + private void psalmSearchButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_psalmSearchButtonActionPerformed + String psalm = AnalysisUiUtils.searchPsalm(); + if (psalm != null) { + psalmTextField.setText(psalm); + } + }//GEN-LAST:event_psalmSearchButtonActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton psalmBrowseButton; + private javax.swing.JButton psalmConfigurationBrowseButton; + private javax.swing.JLabel psalmConfigurationLabel; + private javax.swing.JTextField psalmConfigurationTextField; + private javax.swing.JCheckBox psalmEnabledCheckBox; + private javax.swing.JLabel psalmLabel; + private javax.swing.JComboBox psalmLevelComboBox; + private javax.swing.JLabel psalmLevelLabel; + private javax.swing.JLabel psalmMemoryLimitLabel; + private javax.swing.JTextField psalmMemoryLimitTextField; + private javax.swing.JButton psalmSearchButton; + private javax.swing.JTextField psalmTextField; + // End of variables declaration//GEN-END:variables +} diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/AnalysisCategoryPanels.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/AnalysisCategoryPanels.java index 827fd6d82719..d2bd61a2391a 100644 --- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/AnalysisCategoryPanels.java +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/AnalysisCategoryPanels.java @@ -41,7 +41,8 @@ public static Collection getCategoryPanels() { new CodeSnifferOptionsPanel(), new MessDetectorOptionsPanel(), new CodingStandardsFixerOptionsPanel(), - new PHPStanOptionsPanel()); + new PHPStanOptionsPanel(), + new PsalmOptionsPanel()); } } diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/Bundle.properties b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/Bundle.properties index 5f0f6d551247..5ae554b1420d 100644 --- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/Bundle.properties +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/Bundle.properties @@ -52,6 +52,8 @@ CodingStandardsFixerOptionsPanel.codingStandardsFixerLevelLabel.text=Default &Le CodingStandardsFixerOptionsPanel.codingStandardsFixerOptionsLabel.text=Default &Options: CodingStandardsFixerOptionsPanel.codingStandardsFixerOptionsTextField.text= CodingStandardsFixerOptionsPanel.codingStandardsFixerVersionLabel.text=Default &Version: + +# phpstan PHPStanOptionsPanel.phpStanLabel.text=&PHPStan: PHPStanOptionsPanel.phpStanBrowseButton.text=&Browse... PHPStanOptionsPanel.phpStanSearchButton.text=&Search... @@ -63,3 +65,17 @@ PHPStanOptionsPanel.phpStanMinVersionInfoLabel.text=PHPStan 0.10.3 or newer is s PHPStanOptionsPanel.phpStanLearnMoreLabel.text=Learn more about PHPStan PHPStanOptionsPanel.phpStanMemoryLimitLabel.text=&Memory Limit: PHPStanOptionsPanel.phpStanConfigurationInfoLabel.text=Full configuration file path (typically, phpstan.neon or phpstan.dist.neon) + +# psalm +PsalmOptionsPanel.psalmLabel.text=&Psalm: +PsalmOptionsPanel.psalmLabel.AccessibleContext.accessibleName=&Psalm: +PsalmOptionsPanel.psalmBrowseButton.text=&Browse... +PsalmOptionsPanel.psalmConfigurationLabel.text=&Configuration: +PsalmOptionsPanel.psalmConfiturationBrowseButton.text=B&rowse... +PsalmOptionsPanel.psalmConfigurationInfoLabel.text=Full configuration file path (leave empty if the project root directory already contains psalm.xml) +PsalmOptionsPanel.psalmSearchButton.text=&Search... +PsalmOptionsPanel.psalmLevelLabel.text=&Level: +PsalmOptionsPanel.psalmMemoryLimitLabel.text=&Memory Limit: +PsalmOptionsPanel.psalmNoteLabel.text=Note: +PsalmOptionsPanel.psalmMinVersionInfoLabel.text= +PsalmOptionsPanel.psalmLearnMoreLabel.text=Learn more about Psalm diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PsalmOptionsPanel.form b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PsalmOptionsPanel.form new file mode 100644 index 000000000000..0babf9d6c189 --- /dev/null +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PsalmOptionsPanel.form @@ -0,0 +1,267 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PsalmOptionsPanel.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PsalmOptionsPanel.java new file mode 100644 index 000000000000..fd7ab439b38d --- /dev/null +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PsalmOptionsPanel.java @@ -0,0 +1,387 @@ +/* + * 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.netbeans.modules.php.analysis.ui.options; + +import java.awt.Cursor; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import javax.swing.DefaultComboBoxModel; +import javax.swing.GroupLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.LayoutStyle; +import javax.swing.event.ChangeListener; +import javax.swing.event.DocumentListener; +import org.netbeans.modules.php.analysis.commands.Psalm; +import org.netbeans.modules.php.analysis.options.AnalysisOptions; +import org.netbeans.modules.php.analysis.options.AnalysisOptionsValidator; +import org.netbeans.modules.php.analysis.ui.AnalysisDefaultDocumentListener; +import org.netbeans.modules.php.analysis.options.ValidatorPsalmParameter; +import org.netbeans.modules.php.analysis.util.AnalysisUiUtils; +import org.netbeans.modules.php.api.validation.ValidationResult; +import org.openide.awt.HtmlBrowser; +import org.openide.awt.Mnemonics; +import org.openide.util.ChangeSupport; +import org.openide.util.Exceptions; +import org.openide.util.NbBundle; + +public class PsalmOptionsPanel extends AnalysisCategoryPanel { + + private static final long serialVersionUID = 1199550925948622972L; + + private final ChangeSupport changeSupport = new ChangeSupport(this); + + /** + * Creates new form PsalmOptionsPanel + */ + public PsalmOptionsPanel() { + super(); + initComponents(); + init(); + } + + @NbBundle.Messages({ + "# {0} - short script name", + "# {1} - long script name", + "PsalmOptionsPanel.hint=Full path of Psalm script (typically {0} or {1}).",}) + private void init() { + psalmHintLabel.setText(Bundle.PsalmOptionsPanel_hint(Psalm.NAME, Psalm.LONG_NAME)); + psalmLevelComboBox.removeAllItems(); + // NETBEANS-2974 + // allow empty level option to use a level of a configuration file + psalmLevelComboBox.addItem(""); // NOI18N + for (int i = AnalysisOptions.PSALM_MIN_LEVEL; i <= AnalysisOptions.PSALM_MAX_LEVEL; i++) { + psalmLevelComboBox.addItem(String.valueOf(i)); + } + // add listener + DocumentListener defaultDocumentListener = new AnalysisDefaultDocumentListener(() -> fireChange()); + psalmTextField.getDocument().addDocumentListener(defaultDocumentListener); + psalmConfigurationTextField.getDocument().addDocumentListener(defaultDocumentListener); + psalmMemoryLimitTextField.getDocument().addDocumentListener(defaultDocumentListener); + psalmLevelComboBox.addActionListener(e -> fireChange()); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + psalmLabel = new JLabel(); + psalmTextField = new JTextField(); + psalmBrowseButton = new JButton(); + psalmSearchButton = new JButton(); + psalmHintLabel = new JLabel(); + psalmLevelLabel = new JLabel(); + psalmLevelComboBox = new JComboBox<>(); + psalmMemoryLimitLabel = new JLabel(); + psalmMemoryLimitTextField = new JTextField(); + psalmConfigurationLabel = new JLabel(); + psalmConfigurationTextField = new JTextField(); + psalmConfigurationInfoLabel = new JLabel(); + psalmConfiturationBrowseButton = new JButton(); + psalmNoteLabel = new JLabel(); + psalmMinVersionInfoLabel = new JLabel(); + psalmLearnMoreLabel = new JLabel(); + + psalmLabel.setLabelFor(psalmTextField); + Mnemonics.setLocalizedText(psalmLabel, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmLabel.text")); // NOI18N + + Mnemonics.setLocalizedText(psalmBrowseButton, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmBrowseButton.text")); // NOI18N + psalmBrowseButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + psalmBrowseButtonActionPerformed(evt); + } + }); + + Mnemonics.setLocalizedText(psalmSearchButton, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmSearchButton.text")); // NOI18N + psalmSearchButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + psalmSearchButtonActionPerformed(evt); + } + }); + + Mnemonics.setLocalizedText(psalmHintLabel, "HINT"); // NOI18N + + psalmLevelLabel.setLabelFor(psalmLevelComboBox); + Mnemonics.setLocalizedText(psalmLevelLabel, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmLevelLabel.text")); // NOI18N + + psalmLevelComboBox.setModel(new DefaultComboBoxModel<>(new String[] { "0", "1", "2", "3", "4", "5", "6", "7" })); + + psalmMemoryLimitLabel.setLabelFor(psalmMemoryLimitTextField); + Mnemonics.setLocalizedText(psalmMemoryLimitLabel, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmMemoryLimitLabel.text")); // NOI18N + + psalmConfigurationLabel.setLabelFor(psalmConfigurationTextField); + Mnemonics.setLocalizedText(psalmConfigurationLabel, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmConfigurationLabel.text")); // NOI18N + + Mnemonics.setLocalizedText(psalmConfigurationInfoLabel, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmConfigurationInfoLabel.text")); // NOI18N + + Mnemonics.setLocalizedText(psalmConfiturationBrowseButton, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmConfiturationBrowseButton.text")); // NOI18N + psalmConfiturationBrowseButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + psalmConfiturationBrowseButtonActionPerformed(evt); + } + }); + + Mnemonics.setLocalizedText(psalmNoteLabel, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmNoteLabel.text")); // NOI18N + + Mnemonics.setLocalizedText(psalmMinVersionInfoLabel, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmMinVersionInfoLabel.text")); // NOI18N + + Mnemonics.setLocalizedText(psalmLearnMoreLabel, NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmLearnMoreLabel.text")); // NOI18N + psalmLearnMoreLabel.addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent evt) { + psalmLearnMoreLabelMousePressed(evt); + } + public void mouseEntered(MouseEvent evt) { + psalmLearnMoreLabelMouseEntered(evt); + } + }); + + GroupLayout layout = new GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(psalmMinVersionInfoLabel) + .addComponent(psalmLearnMoreLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(psalmConfigurationLabel) + .addComponent(psalmLabel) + .addComponent(psalmLevelLabel) + .addComponent(psalmNoteLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(psalmMemoryLimitLabel)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(psalmConfigurationInfoLabel) + .addComponent(psalmLevelComboBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(psalmMemoryLimitTextField, GroupLayout.PREFERRED_SIZE, 100, GroupLayout.PREFERRED_SIZE)) + .addGap(0, 62, Short.MAX_VALUE)) + .addGroup(GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING) + .addComponent(psalmConfigurationTextField) + .addComponent(psalmTextField, GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addComponent(psalmHintLabel) + .addGap(0, 0, Short.MAX_VALUE))) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(psalmBrowseButton) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(psalmSearchButton)) + .addComponent(psalmConfiturationBrowseButton))))) + ); + layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(psalmLabel) + .addComponent(psalmTextField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(psalmBrowseButton) + .addComponent(psalmSearchButton)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(psalmHintLabel) + .addGap(6, 6, 6) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(psalmConfigurationLabel) + .addComponent(psalmConfigurationTextField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(psalmConfiturationBrowseButton)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(psalmConfigurationInfoLabel) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(psalmLevelLabel) + .addComponent(psalmLevelComboBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(psalmMemoryLimitLabel) + .addComponent(psalmMemoryLimitTextField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addGap(18, 18, 18) + .addComponent(psalmNoteLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(psalmMinVersionInfoLabel) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(psalmLearnMoreLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + ); + + psalmLabel.getAccessibleContext().setAccessibleName(NbBundle.getMessage(PsalmOptionsPanel.class, "PsalmOptionsPanel.psalmLabel.AccessibleContext.accessibleName")); // NOI18N + }// //GEN-END:initComponents + + private void psalmBrowseButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_psalmBrowseButtonActionPerformed + File file = AnalysisUiUtils.browsePsalm(); + if (file != null) { + psalmTextField.setText(file.getAbsolutePath()); + } + }//GEN-LAST:event_psalmBrowseButtonActionPerformed + + private void psalmSearchButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_psalmSearchButtonActionPerformed + String psalm = AnalysisUiUtils.searchPsalm(); + if (psalm != null) { + psalmTextField.setText(psalm); + } + }//GEN-LAST:event_psalmSearchButtonActionPerformed + + private void psalmLearnMoreLabelMouseEntered(MouseEvent evt) {//GEN-FIRST:event_psalmLearnMoreLabelMouseEntered + evt.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + }//GEN-LAST:event_psalmLearnMoreLabelMouseEntered + + private void psalmLearnMoreLabelMousePressed(MouseEvent evt) {//GEN-FIRST:event_psalmLearnMoreLabelMousePressed + try { + URL url = new URL("https://github.com/vimeo/psalm"); // NOI18N + HtmlBrowser.URLDisplayer.getDefault().showURL(url); + } catch (MalformedURLException ex) { + Exceptions.printStackTrace(ex); + } + }//GEN-LAST:event_psalmLearnMoreLabelMousePressed + + private void psalmConfiturationBrowseButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_psalmConfiturationBrowseButtonActionPerformed + File file = AnalysisUiUtils.browsePsalmConfiguration(); + if (file != null) { + psalmConfigurationTextField.setText(file.getAbsolutePath()); + } + }//GEN-LAST:event_psalmConfiturationBrowseButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private JButton psalmBrowseButton; + private JLabel psalmConfigurationInfoLabel; + private JLabel psalmConfigurationLabel; + private JTextField psalmConfigurationTextField; + private JButton psalmConfiturationBrowseButton; + private JLabel psalmHintLabel; + private JLabel psalmLabel; + private JLabel psalmLearnMoreLabel; + private JComboBox psalmLevelComboBox; + private JLabel psalmLevelLabel; + private JLabel psalmMemoryLimitLabel; + private JTextField psalmMemoryLimitTextField; + private JLabel psalmMinVersionInfoLabel; + private JLabel psalmNoteLabel; + private JButton psalmSearchButton; + private JTextField psalmTextField; + // End of variables declaration//GEN-END:variables + + @NbBundle.Messages("PsalmOptionsPanel.category.name=Psalm") + @Override + public String getCategoryName() { + return Bundle.PsalmOptionsPanel_category_name(); + } + + @Override + public void addChangeListener(ChangeListener listener) { + changeSupport.addChangeListener(listener); + } + + @Override + public void removeChangeListener(ChangeListener listener) { + changeSupport.removeChangeListener(listener); + } + + @Override + public void update() { + AnalysisOptions options = AnalysisOptions.getInstance(); + setPsalmPath(options.getPsalmPath()); + setPsalmConfigurationPath(options.getPsalmConfigurationPath()); + setPsalmLevel(options.getPsalmLevel()); + setPsalmMemoryLimit(options.getPsalmMemoryLimit()); + } + + @Override + public void applyChanges() { + AnalysisOptions options = AnalysisOptions.getInstance(); + options.setPsalmPath(getPsalmPath()); + options.setPsalmConfigurationPath(getPsalmConfigurationPath()); + options.setPsalmLevel(getPsalmLevel()); + options.setPsalmMemoryLimit(getPsalmMemoryLimit()); + } + + @Override + public boolean isChanged() { + String saved = AnalysisOptions.getInstance().getPsalmPath(); + String current = getPsalmPath(); + if (saved == null ? !current.isEmpty() : !saved.equals(current)) { + return true; + } + saved = AnalysisOptions.getInstance().getPsalmConfigurationPath(); + current = getPsalmConfigurationPath(); + if (saved == null ? !current.isEmpty() : !saved.equals(current)) { + return true; + } + String savedString = AnalysisOptions.getInstance().getPsalmLevel(); + String currentString = getPsalmLevel(); + return !savedString.equals(currentString); + } + + @Override + public ValidationResult getValidationResult() { + return new AnalysisOptionsValidator() + .validatePsalm(ValidatorPsalmParameter.create(this)) + .getResult(); + } + + void fireChange() { + changeSupport.fireChange(); + } + + public String getPsalmPath() { + return psalmTextField.getText().trim(); + } + + private void setPsalmPath(String path) { + psalmTextField.setText(path); + } + + public String getPsalmConfigurationPath() { + return psalmConfigurationTextField.getText().trim(); + } + + private void setPsalmConfigurationPath(String path) { + psalmConfigurationTextField.setText(path); + } + + public String getPsalmLevel() { + return (String) psalmLevelComboBox.getSelectedItem(); + } + + private void setPsalmLevel(String level) { + psalmLevelComboBox.setSelectedItem(level); + } + + public String getPsalmMemoryLimit() { + return psalmMemoryLimitTextField.getText().trim(); + } + + private void setPsalmMemoryLimit(String memoryLimit) { + psalmMemoryLimitTextField.setText(memoryLimit); + } +} diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/resources/psalm.png b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/resources/psalm.png new file mode 100644 index 0000000000000000000000000000000000000000..80b1b4e552ce90a870eda4f825e46a34de923b7b GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1|*LJgC&E6~}f&c$&vyT0*x1ZmXDIl+MZo{ti zPyU~eP5uAh<_7C=1v`&93$>5G|Nq`t*uVhD-fr^ne|O#S|MuHO4;catVfXZVqr>z6 ze^S%Q|MR1R4uTA3w_df^APs1;KF{{w{EHlGCRi}YOs!!OvN^*DbP|K7tDnm{r-UW| DjgDV| literal 0 HcmV?d00001 diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/util/AnalysisUiUtils.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/util/AnalysisUiUtils.java index 901e024846a8..6fa40d23757b 100644 --- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/util/AnalysisUiUtils.java +++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/util/AnalysisUiUtils.java @@ -28,6 +28,7 @@ import org.netbeans.modules.php.analysis.commands.CodingStandardsFixer; import org.netbeans.modules.php.analysis.commands.MessDetector; import org.netbeans.modules.php.analysis.commands.PHPStan; +import org.netbeans.modules.php.analysis.commands.Psalm; import org.netbeans.modules.php.api.util.FileUtils; import org.netbeans.modules.php.api.util.UiUtils; import org.openide.filesystems.FileChooserBuilder; @@ -41,6 +42,8 @@ public final class AnalysisUiUtils { private static final String MESS_DETECTOR_RULE_SET_FILE_LAST_FOLDER_SUFFIX = ".messDetector.ruleSetFile"; // NOI18N private static final String PHPSTAN_LAST_FOLDER_SUFFIX = ".phpstan"; // NOI18N private static final String PHPSTAN_CONFIGURATION_LAST_FOLDER_SUFFIX = ".phpstan.config"; // NOI18N + private static final String PSALM_LAST_FOLDER_SUFFIX = ".psalm"; // NOI18N + private static final String PSALM_CONFIGURATION_LAST_FOLDER_SUFFIX = ".psalm.config"; // NOI18N private AnalysisUiUtils() { } @@ -81,6 +84,18 @@ public static File browsePHPStanConfiguration() { return browse(PHPSTAN_CONFIGURATION_LAST_FOLDER_SUFFIX, Bundle.AnalysisUiUtils_browse_phpstan_configuration_title()); } + @CheckForNull + @NbBundle.Messages("AnalysisUiUtils.browse.psalm.title=Select Psalm") + public static File browsePsalm() { + return browse(PSALM_LAST_FOLDER_SUFFIX, Bundle.AnalysisUiUtils_browse_psalm_title()); + } + + @CheckForNull + @NbBundle.Messages("AnalysisUiUtils.browse.psalm.configuration.title=Select Psalm Configuration File") + public static File browsePsalmConfiguration() { + return browse(PSALM_CONFIGURATION_LAST_FOLDER_SUFFIX, Bundle.AnalysisUiUtils_browse_psalm_configuration_title()); + } + @CheckForNull private static File browse(String lastFolderSuffix, String title) { File file = new FileChooserBuilder(AnalysisUiUtils.class.getName() + lastFolderSuffix) @@ -158,6 +173,23 @@ public static String searchPHPStan() { return search(param); } + @CheckForNull + @NbBundle.Messages({ + "AnalysisUiUtils.search.psalm.title=Psalm scripts", + "AnalysisUiUtils.search.psalm.scripts=P&salm scripts:", + "AnalysisUiUtils.search.psalm.pleaseWaitPart=Psalm scripts", + "AnalysisUiUtils.search.psalm.notFound=No Psalm scripts found." + }) + public static String searchPsalm() { + SearchParameter param = new SearchParameter() + .setFilenames(Arrays.asList(Psalm.NAME, Psalm.LONG_NAME)) + .setWindowTitle(Bundle.AnalysisUiUtils_search_psalm_title()) + .setListTitle(Bundle.AnalysisUiUtils_search_psalm_scripts()) + .setPleaseWaitPart(Bundle.AnalysisUiUtils_search_psalm_pleaseWaitPart()) + .setNoItemsFound(Bundle.AnalysisUiUtils_search_psalm_notFound()); + return search(param); + } + @CheckForNull private static String search(SearchParameter param) { return UiUtils.SearchWindow.search(new UiUtils.SearchWindow.SearchWindowSupport() { diff --git a/php/php.code.analysis/test/unit/data/psalm/PsalmSupport/src/Calculator.php b/php/php.code.analysis/test/unit/data/psalm/PsalmSupport/src/Calculator.php new file mode 100644 index 000000000000..90c534d7f0f8 --- /dev/null +++ b/php/php.code.analysis/test/unit/data/psalm/PsalmSupport/src/Calculator.php @@ -0,0 +1,3 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/php/php.code.analysis/test/unit/src/org/netbeans/modules/php/analysis/parsers/PHPStanReportParserTest.java b/php/php.code.analysis/test/unit/src/org/netbeans/modules/php/analysis/parsers/CheckStyleReportParserTest.java similarity index 71% rename from php/php.code.analysis/test/unit/src/org/netbeans/modules/php/analysis/parsers/PHPStanReportParserTest.java rename to php/php.code.analysis/test/unit/src/org/netbeans/modules/php/analysis/parsers/CheckStyleReportParserTest.java index 42786d753be2..d8a5082643a8 100644 --- a/php/php.code.analysis/test/unit/src/org/netbeans/modules/php/analysis/parsers/PHPStanReportParserTest.java +++ b/php/php.code.analysis/test/unit/src/org/netbeans/modules/php/analysis/parsers/CheckStyleReportParserTest.java @@ -32,16 +32,16 @@ import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; -public class PHPStanReportParserTest extends NbTestCase { +public class CheckStyleReportParserTest extends NbTestCase { - public PHPStanReportParserTest(String name) { + public CheckStyleReportParserTest(String name) { super(name); } public void testParse() throws Exception { FileObject root = getDataDir("phpstan/PHPStanSupport"); FileObject workDir = root; - List results = PHPStanReportParser.parse(getLogFile("phpstan-log.xml"), root, workDir); + List results = CheckStyleReportParser.parse(getLogFile("phpstan-log.xml"), root, workDir); assertNotNull(results); assertEquals(4, results.size()); @@ -67,7 +67,7 @@ public void testParse() throws Exception { public void testParseWithOtherOutput() throws Exception { FileObject root = getDataDir("phpstan/PHPStanSupport"); FileObject workDir = root; - List results = PHPStanReportParser.parse(getLogFile("phpstan-log-with-other-output.xml"), root, workDir); + List results = CheckStyleReportParser.parse(getLogFile("phpstan-log-with-other-output.xml"), root, workDir); assertNotNull(results); assertEquals(2, results.size()); } @@ -75,7 +75,7 @@ public void testParseWithOtherOutput() throws Exception { public void testParseNetBeans3022() throws Exception { FileObject root = getDataDir("phpstan/PHPStanSupport/netbeans3022"); FileObject workDir = getDataDir("phpstan/PHPStanSupport"); - List results = PHPStanReportParser.parse(getLogFile("phpstan-log-netbeans-3022.xml"), root, workDir); + List results = CheckStyleReportParser.parse(getLogFile("phpstan-log-netbeans-3022.xml"), root, workDir); assertNotNull(results); assertEquals(3, results.size()); } @@ -83,7 +83,7 @@ public void testParseNetBeans3022() throws Exception { public void testParseNetBeans3022Win() throws Exception { FileObject root = getDataDir("phpstan/PHPStanSupport/netbeans3022"); FileObject workDir = getDataDir("phpstan/PHPStanSupport"); - List results = PHPStanReportParser.parse(getLogFile("phpstan-log-netbeans-3022-win.xml"), root, workDir); + List results = CheckStyleReportParser.parse(getLogFile("phpstan-log-netbeans-3022-win.xml"), root, workDir); assertNotNull(results); assertEquals(3, results.size()); } @@ -93,7 +93,7 @@ public void testParseNetBeans3022WithoutWorkDir() throws Exception { FileObject workDir = null; File logFile = getLogFile("phpstan-log-netbeans-3022-without-workdir.xml"); fixContent(logFile); - List results = PHPStanReportParser.parse(logFile, root, workDir); + List results = CheckStyleReportParser.parse(logFile, root, workDir); assertNotNull(results); assertEquals(3, results.size()); } @@ -101,7 +101,7 @@ public void testParseNetBeans3022WithoutWorkDir() throws Exception { public void testParseWithHtmlEntities() throws Exception { FileObject root = getDataDir("phpstan/PHPStanSupport"); FileObject workDir = root; - List results = PHPStanReportParser.parse(getLogFile("phpstan-log-html-entities.xml"), root, workDir); + List results = CheckStyleReportParser.parse(getLogFile("phpstan-log-html-entities.xml"), root, workDir); assertNotNull(results); assertEquals(1, results.size()); @@ -112,6 +112,26 @@ public void testParseWithHtmlEntities() throws Exception { assertEquals("Function count() should return int but returns array<string>.", result.getDescription()); } + public void testPsalmParse() throws Exception { + FileObject root = getDataDir("psalm/PsalmSupport"); + FileObject workDir = root; + List results = CheckStyleReportParser.parse(getPsalmLogFile("nb-php-psalm-log.xml"), root, workDir); + assertNotNull(results); + + assertEquals(40, results.size()); + Result result = results.get(0); + assertEquals(FileUtil.toFile(root.getFileObject("src/Calculator.php")).getAbsolutePath(), result.getFilePath()); + assertEquals(32, result.getLine()); + assertEquals("error: MissingReturnType: Method Calculator::plus does not have a return type", result.getCategory()); + assertEquals("MissingReturnType: Method Calculator::plus does not have a return type", result.getDescription()); + + result = results.get(23); + assertEquals(FileUtil.toFile(root.getFileObject("test/src/CalculatorTest.php")).getAbsolutePath(), result.getFilePath()); + assertEquals(46, result.getLine()); + assertEquals("error: MissingReturnType: Method CalculatorTest::testPlus does not have a return type, expecting void", result.getCategory()); + assertEquals("MissingReturnType: Method CalculatorTest::testPlus does not have a return type, expecting void", result.getDescription()); + } + private File getLogFile(String name) throws Exception { assertNotNull(name); File phpstan = new File(getDataDir(), "phpstan"); @@ -120,6 +140,14 @@ private File getLogFile(String name) throws Exception { return xmlLog; } + private File getPsalmLogFile(String name) throws Exception { + assertNotNull(name); + File psalm = new File(getDataDir(), "psalm"); + File xmlLog = new File(psalm, name); + assertTrue(xmlLog.isFile()); + return xmlLog; + } + private FileObject getDataDir(String name) { assertNotNull(name); FileObject dataDir = FileUtil.toFileObject(getDataDir());