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 extends ErrorDescription> 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 extends ErrorDescription> 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 extends WarningDescription> 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..760727d779af
--- /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());
+ }
+ }
+ });
+ }
+
+ 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..178d157c5cff 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
@@ -48,10 +48,10 @@
/**
* Parser for PHPStan xml report file.
*/
-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 000000000000..80b1b4e552ce
Binary files /dev/null and b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/resources/psalm.png differ
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/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 87%
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..a955d5fb74de 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());