You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@netbeans.apache.org by GitBox <gi...@apache.org> on 2018/09/25 03:50:53 UTC

[GitHub] tmysik closed pull request #896: [NETBEANS-1276] PHPStan Support

tmysik closed pull request #896: [NETBEANS-1276] PHPStan Support
URL: https://github.com/apache/incubator-netbeans/pull/896
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/php/php.code.analysis/licenseinfo.xml b/php/php.code.analysis/licenseinfo.xml
index fd4216ba4c..2b37e25714 100644
--- a/php/php.code.analysis/licenseinfo.xml
+++ b/php/php.code.analysis/licenseinfo.xml
@@ -24,6 +24,7 @@
         <file>src/org/netbeans/modules/php/analysis/ui/resources/code-sniffer.png</file>
         <file>src/org/netbeans/modules/php/analysis/ui/resources/coding-standards-fixer.png</file>
         <file>src/org/netbeans/modules/php/analysis/ui/resources/mess-detector.png</file>
+        <file>src/org/netbeans/modules/php/analysis/ui/resources/phpstan.png</file>
         <license ref="Apache-2.0-ASF" />
         <comment type="COMMENT_UNSUPPORTED" />
     </fileset>
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PHPStanAnalyzerImpl.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PHPStanAnalyzerImpl.java
new file mode 100644
index 0000000000..c97a14d424
--- /dev/null
+++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PHPStanAnalyzerImpl.java
@@ -0,0 +1,248 @@
+/*
+ * 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.PHPStan;
+import org.netbeans.modules.php.analysis.options.AnalysisOptions;
+import org.netbeans.modules.php.analysis.ui.analyzer.PHPStanCustomizerPanel;
+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 PHPStanAnalyzerImpl implements Analyzer {
+
+    private static final Logger LOGGER = Logger.getLogger(PHPStanAnalyzerImpl.class.getName());
+    private final Context context;
+    private final AtomicBoolean cancelled = new AtomicBoolean();
+
+    public PHPStanAnalyzerImpl(Context context) {
+        this.context = context;
+    }
+
+    @NbBundle.Messages({
+        "PHPStanAnalyzerImpl.phpStan.error=PHPStan is not valid",
+        "PHPStanAnalyzerImpl.phpStan.error.description=Invalid phpstan set in IDE Options."
+    })
+    @Override
+    public Iterable<? extends ErrorDescription> analyze() {
+        Preferences settings = context.getSettings();
+        if (settings != null && !settings.getBoolean(PHPStanCustomizerPanel.ENABLED, false)) {
+            return Collections.emptyList();
+        }
+
+        PHPStan phpStan = getValidPHPStan();
+        if (phpStan == null) {
+            context.reportAnalysisProblem(
+                    Bundle.PHPStanAnalyzerImpl_phpStan_error(),
+                    Bundle.PHPStanAnalyzerImpl_phpStan_error_description());
+            return Collections.emptyList();
+        }
+
+        String level = getValidPHPStanLevel();
+        FileObject config = getValidPHPStanConfiguration();
+        PHPStanParams phpStanParams = new PHPStanParams()
+                .setLevel(level)
+                .setConfiguration(config);
+        Scope scope = context.getScope();
+
+        Map<FileObject, Integer> fileCount = AnalysisUtils.countPhpFiles(scope);
+        int totalCount = 0;
+        for (Integer count : fileCount.values()) {
+            totalCount += count;
+        }
+
+        context.start(totalCount);
+        try {
+            return doAnalyze(scope, phpStan, phpStanParams, fileCount);
+        } finally {
+            context.finish();
+        }
+    }
+
+    @Override
+    public boolean cancel() {
+        cancelled.set(true);
+        return true;
+    }
+
+    @NbBundle.Messages({
+        "PHPStanAnalyzerImpl.analyze.error=PHPStan analysis error",
+        "PHPStanAnalyzerImpl.analyze.error.description=Error occurred during phpstan analysis, review Output window for more information."
+    })
+    private Iterable<? extends ErrorDescription> doAnalyze(Scope scope, PHPStan phpStan,
+            PHPStanParams params, Map<FileObject, Integer> fileCount) {
+        List<ErrorDescription> errors = new ArrayList<>();
+        int progress = 0;
+        phpStan.startAnalyzeGroup();
+        for (FileObject root : scope.getSourceRoots()) {
+            if (cancelled.get()) {
+                return Collections.emptyList();
+            }
+            List<org.netbeans.modules.php.analysis.results.Result> results = phpStan.analyze(params, root);
+            if (results == null) {
+                context.reportAnalysisProblem(
+                        Bundle.PHPStanAnalyzerImpl_analyze_error(),
+                        Bundle.PHPStanAnalyzerImpl_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<org.netbeans.modules.php.analysis.results.Result> results = phpStan.analyze(params, file);
+            if (results == null) {
+                context.reportAnalysisProblem(
+                        Bundle.PHPStanAnalyzerImpl_analyze_error(),
+                        Bundle.PHPStanAnalyzerImpl_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<org.netbeans.modules.php.analysis.results.Result> results = phpStan.analyze(params, folder);
+            if (results == null) {
+                context.reportAnalysisProblem(
+                        Bundle.PHPStanAnalyzerImpl_analyze_error(),
+                        Bundle.PHPStanAnalyzerImpl_analyze_error_description());
+                return Collections.emptyList();
+            }
+            errors.addAll(Mappers.map(results));
+            progress += fileCount.get(folder);
+            context.progress(progress);
+        }
+        return errors;
+    }
+
+    @CheckForNull
+    private PHPStan getValidPHPStan() {
+        try {
+            return PHPStan.getDefault();
+        } catch (InvalidPhpExecutableException ex) {
+            LOGGER.log(Level.INFO, null, ex);
+        }
+        return null;
+    }
+
+    @CheckForNull
+    private String getValidPHPStanLevel() {
+        String phpStanLevel = null;
+        Preferences settings = context.getSettings();
+        if (settings != null) {
+            phpStanLevel = settings.get(PHPStanCustomizerPanel.LEVEL, null);
+        }
+        if (phpStanLevel == null) {
+            phpStanLevel = String.valueOf(AnalysisOptions.getInstance().getPHPStanLevel());
+        }
+        assert phpStanLevel != null;
+        return phpStanLevel;
+
+    }
+
+    @CheckForNull
+    private FileObject getValidPHPStanConfiguration() {
+        String phpStanConfiguration = null;
+        Preferences settings = context.getSettings();
+        if (settings != null) {
+            phpStanConfiguration = settings.get(PHPStanCustomizerPanel.CONFIGURATION, null);
+        }
+        if (phpStanConfiguration == null) {
+            phpStanConfiguration = AnalysisOptions.getInstance().getPHPStanConfigurationPath();
+        }
+        if (StringUtils.isEmpty(phpStanConfiguration)) {
+            return null;
+        }
+        return FileUtil.toFileObject(new File(phpStanConfiguration));
+    }
+
+    //~ Inner class
+    @ServiceProvider(service = AnalyzerFactory.class)
+    public static final class PHPStanAnalyzerFactory extends AnalyzerFactory {
+
+        @StaticResource
+        private static final String ICON_PATH = "org/netbeans/modules/php/analysis/ui/resources/phpstan.png"; // NOI18N
+
+        @NbBundle.Messages("PHPStanAnalyzerFactory.displayName=PHPStan")
+        public PHPStanAnalyzerFactory() {
+            super("PHPStan", Bundle.PHPStanAnalyzerFactory_displayName(), ICON_PATH);
+        }
+
+        @Override
+        public Iterable<? extends WarningDescription> getWarnings() {
+            return Collections.emptyList();
+        }
+
+        @Override
+        public CustomizerProvider<Void, PHPStanCustomizerPanel> getCustomizerProvider() {
+            return new CustomizerProvider<Void, PHPStanCustomizerPanel>() {
+                @Override
+                public Void initialize() {
+                    return null;
+                }
+
+                @Override
+                public PHPStanCustomizerPanel createComponent(CustomizerContext<Void, PHPStanCustomizerPanel> context) {
+                    return new PHPStanCustomizerPanel(context);
+                }
+            };
+        }
+
+        @Override
+        public Analyzer createAnalyzer(Context context) {
+            return new PHPStanAnalyzerImpl(context);
+        }
+
+        @Override
+        public void warningOpened(ErrorDescription warning) {
+            HintsController.setErrors(warning.getFile(), "phpStanWarning", Collections.singleton(warning)); // NOI18N
+        }
+    }
+}
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PHPStanParams.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PHPStanParams.java
new file mode 100644
index 0000000000..e84137af85
--- /dev/null
+++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/PHPStanParams.java
@@ -0,0 +1,45 @@
+/*
+ * 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 PHPStanParams {
+
+    private String level;
+    private FileObject configuration;
+
+    public String getLevel() {
+        return level;
+    }
+
+    public FileObject getConfiguration() {
+        return configuration;
+    }
+
+    public PHPStanParams setLevel(String level) {
+        this.level = level;
+        return this;
+    }
+
+    public PHPStanParams setConfiguration(FileObject configuration) {
+        this.configuration = configuration;
+        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
new file mode 100644
index 0000000000..9cb6945d6a
--- /dev/null
+++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/commands/PHPStan.java
@@ -0,0 +1,190 @@
+/*
+ * 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.PHPStanParams;
+import org.netbeans.modules.php.analysis.options.AnalysisOptions;
+import org.netbeans.modules.php.analysis.parsers.PHPStanReportParser;
+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 PHPStan {
+
+    public static final String NAME = "phpstan"; // NOI18N
+    public static final String LONG_NAME = NAME + ".phar"; // NOI18N
+    static final File XML_LOG = new File(System.getProperty("java.io.tmpdir"), "nb-php-phpstan-log.xml"); // NOI18N
+    private static final Logger LOGGER = Logger.getLogger(PHPStan.class.getName());
+
+    // commands
+    private static final String ANALYSE_COMMAND = "analyse"; // NOI18N
+
+    // params
+    private static final String CONFIGURATION_PARAM = "--configuration=%s"; // NOI18N
+    private static final String LEVEL_PARAM = "--level=%s"; // NOI18N
+    private static final String ERROR_FORMAT_PARAM = "--error-format=checkstyle"; // NOI18N Or json, raw, table
+    private static final String NO_PROGRESS_PARAM = "--no-progress"; // NOI18N
+    private static final String NO_INTERACTION_PARAM = "--no-interaction"; // NOI18N
+    private static final String ANSI_PARAM = "--ansi"; // NOI18N
+    private static final String NO_ANSI_PARAM = "--no-ansi"; // NOI18N
+    private static final String VERSION_PARAM = "--version"; // NOI18N
+    private static final String VERBOSE_PARAM = "--verbose"; // NOI18N
+    private static final List<String> ANALYZE_DEFAULT_PARAMS = Arrays.asList(
+            NO_ANSI_PARAM,
+            NO_PROGRESS_PARAM,
+            NO_INTERACTION_PARAM,
+            ERROR_FORMAT_PARAM
+    );
+
+    private final String phpStanPath;
+    private int analyzeGroupCounter = 1;
+
+    private PHPStan(String phpStanPath) {
+        this.phpStanPath = phpStanPath;
+    }
+
+    public static PHPStan getDefault() throws InvalidPhpExecutableException {
+        String phpStanPath = AnalysisOptions.getInstance().getPHPStanPath();
+        String error = validate(phpStanPath);
+        if (error != null) {
+            throw new InvalidPhpExecutableException(error);
+        }
+        return new PHPStan(phpStanPath);
+    }
+
+    @NbBundle.Messages("PHPStan.script.label=PHPStan")
+    public static String validate(String codeSnifferPath) {
+        return PhpExecutableValidator.validateCommand(codeSnifferPath, Bundle.PHPStan_script_label());
+    }
+
+    public void startAnalyzeGroup() {
+        analyzeGroupCounter = 1;
+    }
+
+    @NbBundle.Messages({
+        "# {0} - counter",
+        "PHPStan.analyze=PHPStan (analyze #{0})",})
+    @CheckForNull
+    public List<Result> analyze(PHPStanParams params, FileObject file) {
+        assert file.isValid() : "Invalid file given: " + file;
+        try {
+            Integer result = getExecutable(Bundle.PHPStan_analyze(analyzeGroupCounter++), findWorkDir(file))
+                    .additionalParameters(getParameters(params, file))
+                    .runAndWait(getDescriptor(), "Running phpstan..."); // NOI18N
+            if (result == null) {
+                return null;
+            }
+
+            return PHPStanReportParser.parse(XML_LOG, file);
+        } 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 phpstan.neon}, {@code phpstan.neon.dist}.
+     *
+     * @param file file to find project directory for
+     * @return project directory or {@code null}
+     */
+    @CheckForNull
+    private File findWorkDir(FileObject file) {
+        assert file != null;
+        Project project = FileOwnerQuery.getOwner(file);
+        if (project != null) {
+            File projectDir = FileUtil.toFile(project.getProjectDirectory());
+            if (LOGGER.isLoggable(Level.FINE)) {
+                LOGGER.log(Level.FINE, "Project directory for {0} found in {1}", new Object[]{FileUtil.toFile(file), projectDir});
+            }
+            return projectDir;
+        }
+        return null;
+    }
+
+    private PhpExecutable getExecutable(String title, @NullAllowed File workDir) {
+        PhpExecutable executable = new PhpExecutable(phpStanPath)
+                .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<String> getParameters(PHPStanParams parameters, FileObject file) {
+        // analyse /path/to/{dir|file}
+        List<String> params = new ArrayList<>();
+        params.add(ANALYSE_COMMAND);
+        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()));
+        }
+        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 a51ae9cb45..eb8b4b5028 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
@@ -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.MessDetector;
+import org.netbeans.modules.php.analysis.commands.PHPStan;
 import org.netbeans.modules.php.analysis.util.AnalysisUtils;
 import org.netbeans.modules.php.api.util.FileUtils;
 import org.openide.util.NbPreferences;
@@ -48,11 +49,15 @@
     private static final String CODING_STANDARDS_FIXER_LEVEL = "codingStandardsFixer.level"; // NOI18N
     private static final String CODING_STANDARDS_FIXER_CONFIG = "codingStandardsFixer.config"; // NOI18N
     private static final String CODING_STANDARDS_FIXER_OPTIONS = "codingStandardsFixer.options"; // NOI18N
+    // PHPStan - PHP Static Analysis Tool
+    private static final String PHPSTAN_PATH = "phpstan.path"; // NOI18N
+    private static final String PHPSTAN_LEVEL = "phpstan.level"; // NOI18N
+    private static final String PHPSTAN_CONFIGURATION = "phpstan.configuration"; // NOI18N
 
     private volatile boolean codeSnifferSearched = false;
     private volatile boolean messDetectorSearched = false;
     private volatile boolean codingStandardsFixerSearched = false;
-
+    private volatile boolean phpstanSearched = false;
 
     private AnalysisOptions() {
     }
@@ -61,6 +66,7 @@ public static AnalysisOptions getInstance() {
         return INSTANCE;
     }
 
+    // code sniffer
     @CheckForNull
     public String getCodeSnifferPath() {
         String codeSnifferPath = getPreferences().get(CODE_SNIFFER_PATH, null);
@@ -92,6 +98,7 @@ public void setCodeSnifferStandard(String standard) {
         getPreferences().put(CODE_SNIFFER_STANDARD, standard);
     }
 
+    // mess detector
     @CheckForNull
     public String getMessDetectorPath() {
         String messDetectorPath = getPreferences().get(MESS_DETECTOR_PATH, null);
@@ -122,6 +129,7 @@ public void setMessDetectorRuleSets(List<String> ruleSets) {
         getPreferences().put(MESS_DETECTOR_RULE_SETS, AnalysisUtils.serialize(ruleSets));
     }
 
+    // coding standards fixer
     @CheckForNull
     public String getCodingStandardsFixerVersion() {
         return getPreferences().get(CODING_STANDARDS_FIXER_VERSION, CodingStandardsFixer.VERSIONS.get(CodingStandardsFixer.VERSIONS.size() - 1));
@@ -181,6 +189,50 @@ public void setCodingStandardsFixerOptions(String options) {
         getPreferences().put(CODING_STANDARDS_FIXER_OPTIONS, options);
     }
 
+    // phpstan
+    @CheckForNull
+    public String getPHPStanPath() {
+        String phpstanPath = getPreferences().get(PHPSTAN_PATH, null);
+        if (phpstanPath == null && !phpstanSearched) {
+            phpstanSearched = true;
+            List<String> scripts = FileUtils.findFileOnUsersPath(PHPStan.NAME, PHPStan.LONG_NAME);
+            if (!scripts.isEmpty()) {
+                phpstanPath = scripts.get(0);
+                setMessDetectorPath(phpstanPath);
+            }
+        }
+        return phpstanPath;
+    }
+
+    public void setPHPStanPath(String path) {
+        getPreferences().put(PHPSTAN_PATH, path);
+    }
+
+    public int getPHPStanLevel() {
+        int level = getPreferences().getInt(PHPSTAN_LEVEL, 0);
+        if (level < 0 || 7 < level) {
+            level = 0;
+        }
+        return level;
+    }
+
+    public void setPHPStanLevel(int level) {
+        int l = level;
+        if (level < 0 || 7 < level) {
+            l = 0;
+        }
+        getPreferences().putInt(PHPSTAN_LEVEL, l);
+    }
+
+    @CheckForNull
+    public String getPHPStanConfigurationPath() {
+        return getPreferences().get(PHPSTAN_CONFIGURATION, null);
+    }
+
+    public void setPHPStanConfigurationPath(String configuration) {
+        getPreferences().put(PHPSTAN_CONFIGURATION, configuration);
+    }
+
     private Preferences getPreferences() {
         return NbPreferences.forModule(AnalysisOptions.class).node(PREFERENCES_PATH);
     }
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 c993a0cc83..e519245e19 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
@@ -22,6 +22,8 @@
 import org.netbeans.modules.php.analysis.commands.CodeSniffer;
 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.api.util.FileUtils;
 import org.netbeans.modules.php.api.util.StringUtils;
 import org.netbeans.modules.php.api.validation.ValidationResult;
 import org.openide.util.NbBundle;
@@ -30,7 +32,6 @@
 
     private final ValidationResult result = new ValidationResult();
 
-
     public AnalysisOptionsValidator validateCodeSniffer(String codeSnifferPath, String codeSnifferStandard) {
         validateCodeSnifferPath(codeSnifferPath);
         validateCodeSnifferStandard(codeSnifferStandard);
@@ -48,6 +49,12 @@ public AnalysisOptionsValidator validateCodingStandardsFixer(String codingStanda
         return this;
     }
 
+    public AnalysisOptionsValidator validatePHPStan(String phpStanPath, String configuration) {
+        validatePHPStanPath(phpStanPath);
+        validatePHPStanConfiguration(configuration);
+        return this;
+    }
+
     public ValidationResult getResult() {
         return result;
     }
@@ -92,4 +99,22 @@ private AnalysisOptionsValidator validateCodingStandardsFixerPath(String codingS
         return this;
     }
 
+    private AnalysisOptionsValidator validatePHPStanPath(String phpStanPath) {
+        String warning = PHPStan.validate(phpStanPath);
+        if (warning != null) {
+            result.addWarning(new ValidationResult.Message("phpStan.path", warning)); // NOI18N
+        }
+        return this;
+    }
+
+    private AnalysisOptionsValidator validatePHPStanConfiguration(String configuration) {
+        if (!StringUtils.isEmpty(configuration)) {
+            String warning = FileUtils.validateFile("Configuration file", configuration, false); // NOI18N
+            if (warning != null) {
+                result.addWarning(new ValidationResult.Message("phpStan.configuration", warning)); // NOI18N
+            }
+        }
+        return this;
+    }
+
 }
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/PHPStanReportParser.java
new file mode 100644
index 0000000000..cf2741c826
--- /dev/null
+++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/parsers/PHPStanReportParser.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.parsers;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.netbeans.api.annotations.common.CheckForNull;
+import org.netbeans.modules.php.analysis.results.Result;
+import org.netbeans.modules.php.api.util.FileUtils;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Parser for PHPStan xml report file.
+ */
+public class PHPStanReportParser extends DefaultHandler {
+
+    private static final String PHP_EXT = ".php"; // NOI18N
+    private static final Logger LOGGER = Logger.getLogger(PHPStanReportParser.class.getName());
+    private final List<Result> results = new ArrayList<>();
+    private final XMLReader xmlReader;
+
+    private Result currentResult = null;
+    private String currentFile = null;
+    private final FileObject root;
+
+    private PHPStanReportParser(FileObject root) throws SAXException {
+        this.xmlReader = FileUtils.createXmlReader();
+        this.root = root;
+    }
+
+    private static PHPStanReportParser create(Reader reader, FileObject root) throws SAXException, IOException {
+        PHPStanReportParser parser = new PHPStanReportParser(root);
+        parser.xmlReader.setContentHandler(parser);
+        parser.xmlReader.parse(new InputSource(reader));
+        return parser;
+    }
+
+    @CheckForNull
+    public static List<Result> parse(File file, FileObject root) {
+        try {
+            sanitizeFile(file);
+            try (Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) { // NOI18N
+                return create(reader, root).getResults();
+            }
+        } catch (IOException | SAXException ex) {
+            LOGGER.log(Level.INFO, null, ex);
+        }
+        return null;
+    }
+
+    // sanitize file content (the file can contain progress etc. so then it is not a valid XML file)
+    private static void sanitizeFile(File file) throws IOException {
+        String fileName = file.getAbsolutePath();
+        List<String> newLines = new ArrayList<>();
+        boolean content = false;
+        List<String> readAllLines = Files.readAllLines(Paths.get(fileName), StandardCharsets.UTF_8);
+        if (!readAllLines.isEmpty() && readAllLines.get(0).startsWith("<?xml")) { // NOI18N
+            return;
+        }
+        for (String line : readAllLines) {
+            if (!content) {
+                if (line.startsWith("<?xml")) { // NOI18N
+                    content = true;
+                }
+                continue;
+            }
+            if (content) {
+                newLines.add(line);
+                if (line.equals("</checkstyle>")) { // NOI18N
+                    break;
+                }
+            }
+        }
+        Files.write(Paths.get(fileName), newLines, StandardCharsets.UTF_8);
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        if ("file".equals(qName)) { // NOI18N
+            processFileStart(attributes);
+        } else if ("error".equals(qName)) { // NOI18N
+            processResultStart(attributes);
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        if ("file".equals(qName)) { // NOI18N
+            processFileEnd();
+        } else if ("error".equals(qName)) { // NOI18N
+            processResultEnd();
+        }
+    }
+
+    private void processFileStart(Attributes attributes) {
+        assert currentResult == null : currentResult.getFilePath();
+        assert currentFile == null : currentFile;
+
+        currentFile = getCurrentFile(attributes.getValue("name")); // NOI18N
+    }
+
+    private void processFileEnd() {
+        assert currentFile != null;
+        currentFile = null;
+    }
+
+    private void processResultStart(Attributes attributes) {
+        assert currentFile != null;
+        assert currentResult == null : currentResult.getFilePath();
+
+        currentResult = new Result(currentFile);
+        int lineNumber = getInt(attributes, "line"); // NOI18N
+        // line number can be 0
+        // e.g. <error line="0" column="1" severity="error"
+        // message="Class PhpParser\Builder\ClassTest was not found while trying to analyse it - autoloading is probably not configured properly." />
+        if (lineNumber == 0) {
+            lineNumber = 1;
+        }
+        currentResult.setLine(lineNumber);
+        currentResult.setColumn(getInt(attributes, "column")); // NOI18N
+        String message = attributes.getValue("message"); // NOI18N
+        currentResult.setCategory(String.format("%s: %s", attributes.getValue("severity"), message)); // NOI18N
+        currentResult.setDescription(message);
+    }
+
+    private void processResultEnd() {
+        assert currentResult != null;
+        results.add(currentResult);
+        currentResult = null;
+    }
+
+    private int getInt(Attributes attributes, String name) {
+        int i = -1;
+        try {
+            i = Integer.valueOf(attributes.getValue(name));
+        } catch (NumberFormatException exc) {
+            // ignored
+        }
+        return i;
+    }
+
+    private String getCurrentFile(String fileName) {
+        FileObject parent = root.getParent();
+        String sanitizedFileName = sanitizeFileName(fileName);
+        if (parent.isFolder()) {
+            FileObject current = parent.getFileObject(sanitizedFileName);
+            if (current == null) {
+                return null;
+            }
+            return FileUtil.toFile(current).getAbsolutePath();
+        }
+        return sanitizedFileName;
+    }
+
+    private String sanitizeFileName(String fileName) {
+        // e.g. PHPStanSupport/vendor/nette/utils/src/Utils/SmartObject.php (in context of class Nette\Bridges\DITracy\ContainerPanel)
+        if (!fileName.endsWith(PHP_EXT)) {
+            int lastIndexOfPhpExt = fileName.lastIndexOf(PHP_EXT);
+            if (lastIndexOfPhpExt != -1) {
+                return fileName.substring(0, lastIndexOfPhpExt + PHP_EXT.length());
+            }
+        }
+        return fileName;
+    }
+
+    //~ Getters
+    public List<Result> getResults() {
+        return Collections.unmodifiableList(results);
+    }
+}
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 e520adfbc1..c6f61d9870 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
@@ -29,3 +29,8 @@ CodingStandardsFixerCustomizerPanel.versionLabel.text=&Version:
 CodingStandardsFixerCustomizerPanel.enabledCheckBox.text=&Enabled
 CodeSnifferCustomizerPanel.enabledCheckBox.text=&Enabled
 MessDetectorCustomizerPanel.enabledCheckBox.text=&Enabled
+PHPStanCustomizerPanel.phpStanConfigurationBrowseButton.text=&Browse...
+PHPStanCustomizerPanel.phpStanLevelLabel.text=&Level:
+PHPStanCustomizerPanel.phpStanConfigurationTextField.text=
+PHPStanCustomizerPanel.phpStanConfigurationLabel.text=&Configuration:
+PHPStanCustomizerPanel.phpStanEnabledCheckBox.text=&Enabled
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PHPStanCustomizerPanel.form b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PHPStanCustomizerPanel.form
new file mode 100644
index 0000000000..07276ede56
--- /dev/null
+++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PHPStanCustomizerPanel.form
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!--
+
+    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.
+
+-->
+
+<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" attributes="0">
+              <Component id="phpStanEnabledCheckBox" min="-2" max="-2" attributes="0"/>
+              <EmptySpace max="32767" attributes="0"/>
+          </Group>
+          <Group type="102" attributes="0">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="phpStanConfigurationLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanLevelLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Group type="102" attributes="0">
+                      <Component id="phpStanConfigurationTextField" max="32767" attributes="0"/>
+                      <EmptySpace max="-2" attributes="0"/>
+                      <Component id="phpStanConfigurationBrowseButton" min="-2" max="-2" attributes="0"/>
+                  </Group>
+                  <Group type="102" attributes="0">
+                      <Component id="phpStanLevelComboBox" min="-2" max="-2" attributes="0"/>
+                      <EmptySpace min="0" pref="0" max="32767" attributes="0"/>
+                  </Group>
+              </Group>
+          </Group>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" alignment="0" attributes="0">
+              <Component id="phpStanEnabledCheckBox" min="-2" max="-2" attributes="0"/>
+              <EmptySpace min="-2" max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="phpStanConfigurationLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanConfigurationTextField" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanConfigurationBrowseButton" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="phpStanLevelLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanLevelComboBox" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+          </Group>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Component class="javax.swing.JCheckBox" name="phpStanEnabledCheckBox">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/analyzer/Bundle.properties" key="PHPStanCustomizerPanel.phpStanEnabledCheckBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JLabel" name="phpStanConfigurationLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/analyzer/Bundle.properties" key="PHPStanCustomizerPanel.phpStanConfigurationLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JTextField" name="phpStanConfigurationTextField">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/analyzer/Bundle.properties" key="PHPStanCustomizerPanel.phpStanConfigurationTextField.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JButton" name="phpStanConfigurationBrowseButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/analyzer/Bundle.properties" key="PHPStanCustomizerPanel.phpStanConfigurationBrowseButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="phpStanConfigurationBrowseButtonActionPerformed"/>
+      </Events>
+    </Component>
+    <Component class="javax.swing.JLabel" name="phpStanLevelLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/analyzer/Bundle.properties" key="PHPStanCustomizerPanel.phpStanLevelLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JComboBox" name="phpStanLevelComboBox">
+      <Properties>
+        <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor">
+          <StringArray count="8">
+            <StringItem index="0" value="0"/>
+            <StringItem index="1" value="1"/>
+            <StringItem index="2" value="2"/>
+            <StringItem index="3" value="3"/>
+            <StringItem index="4" value="4"/>
+            <StringItem index="5" value="5"/>
+            <StringItem index="6" value="6"/>
+            <StringItem index="7" value="7"/>
+          </StringArray>
+        </Property>
+      </Properties>
+      <AuxValues>
+        <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="&lt;String&gt;"/>
+      </AuxValues>
+    </Component>
+  </SubComponents>
+</Form>
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PHPStanCustomizerPanel.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PHPStanCustomizerPanel.java
new file mode 100644
index 0000000000..b39a80b644
--- /dev/null
+++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/analyzer/PHPStanCustomizerPanel.java
@@ -0,0 +1,214 @@
+/*
+ * 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.io.File;
+import java.util.prefs.Preferences;
+import javax.swing.JPanel;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import org.netbeans.modules.analysis.spi.Analyzer;
+import org.netbeans.modules.php.analysis.options.AnalysisOptions;
+import org.netbeans.modules.php.analysis.ui.options.PHPStanOptionsPanel;
+import org.openide.filesystems.FileChooserBuilder;
+import org.openide.util.NbBundle;
+
+public class PHPStanCustomizerPanel extends JPanel {
+
+    public static final String ENABLED = "phpStan.enabled"; // NOI18N
+    public static final String LEVEL = "phpStan.level"; // NOI18N
+    public static final String CONFIGURATION = "phpStan.configuration"; // NOI18N
+    private static final String PHPSTAN_CONFIGURATION_LAST_FOLDER_SUFFIX = ".phpstan.config"; // NOI18N
+    private static final long serialVersionUID = 2318201027384364349L;
+
+    final Analyzer.CustomizerContext<Void, PHPStanCustomizerPanel> context;
+    final Preferences settings;
+
+    public PHPStanCustomizerPanel(Analyzer.CustomizerContext<Void, PHPStanCustomizerPanel> context) {
+        assert EventQueue.isDispatchThread();
+        assert context != null;
+
+        this.context = context;
+        this.settings = context.getSettings();
+        initComponents();
+        init();
+    }
+
+    private void init() {
+        setEnabledCheckBox();
+        setLevelComboBox();
+        setConfigurationTextField();
+    }
+
+    private void setEnabledCheckBox() {
+        assert EventQueue.isDispatchThread();
+        phpStanEnabledCheckBox.addItemListener(e -> {
+            setAllComponetsEnabled(phpStanEnabledCheckBox.isSelected());
+            setPHPStanEnabled();
+        });
+        boolean isEnabled = settings.getBoolean(ENABLED, false);
+        phpStanEnabledCheckBox.setSelected(isEnabled);
+        setAllComponetsEnabled(isEnabled);
+    }
+
+    private void setLevelComboBox() {
+        assert EventQueue.isDispatchThread();
+        phpStanLevelComboBox.setSelectedItem(settings.get(LEVEL, String.valueOf(AnalysisOptions.getInstance().getPHPStanLevel())));
+        phpStanLevelComboBox.addItemListener(e -> setLevel());
+    }
+
+    private void setConfigurationTextField() {
+        assert EventQueue.isDispatchThread();
+        phpStanConfigurationTextField.setText(settings.get(CONFIGURATION, AnalysisOptions.getInstance().getPHPStanConfigurationPath()));
+        phpStanConfigurationTextField.getDocument().addDocumentListener(new DocumentListener() {
+            @Override
+            public void insertUpdate(DocumentEvent e) {
+                processUpdate();
+            }
+
+            @Override
+            public void removeUpdate(DocumentEvent e) {
+                processUpdate();
+            }
+
+            @Override
+            public void changedUpdate(DocumentEvent e) {
+                processUpdate();
+            }
+
+            private void processUpdate() {
+                setConfiguration();
+            }
+        });
+    }
+
+    private void setPHPStanEnabled() {
+        settings.putBoolean(ENABLED, phpStanEnabledCheckBox.isSelected());
+    }
+
+    private void setLevel() {
+        settings.put(LEVEL, (String) phpStanLevelComboBox.getSelectedItem());
+    }
+
+    private void setConfiguration() {
+        settings.put(CONFIGURATION, phpStanConfigurationTextField.getText().trim());
+    }
+
+    private void setAllComponetsEnabled(boolean isEnabled) {
+        Component[] components = getComponents();
+        for (Component component : components) {
+            if (component != phpStanEnabledCheckBox) {
+                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")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        phpStanEnabledCheckBox = new javax.swing.JCheckBox();
+        phpStanConfigurationLabel = new javax.swing.JLabel();
+        phpStanConfigurationTextField = new javax.swing.JTextField();
+        phpStanConfigurationBrowseButton = new javax.swing.JButton();
+        phpStanLevelLabel = new javax.swing.JLabel();
+        phpStanLevelComboBox = new javax.swing.JComboBox<>();
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanEnabledCheckBox, org.openide.util.NbBundle.getMessage(PHPStanCustomizerPanel.class, "PHPStanCustomizerPanel.phpStanEnabledCheckBox.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanConfigurationLabel, org.openide.util.NbBundle.getMessage(PHPStanCustomizerPanel.class, "PHPStanCustomizerPanel.phpStanConfigurationLabel.text")); // NOI18N
+
+        phpStanConfigurationTextField.setText(org.openide.util.NbBundle.getMessage(PHPStanCustomizerPanel.class, "PHPStanCustomizerPanel.phpStanConfigurationTextField.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanConfigurationBrowseButton, org.openide.util.NbBundle.getMessage(PHPStanCustomizerPanel.class, "PHPStanCustomizerPanel.phpStanConfigurationBrowseButton.text")); // NOI18N
+        phpStanConfigurationBrowseButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                phpStanConfigurationBrowseButtonActionPerformed(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanLevelLabel, org.openide.util.NbBundle.getMessage(PHPStanCustomizerPanel.class, "PHPStanCustomizerPanel.phpStanLevelLabel.text")); // NOI18N
+
+        phpStanLevelComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "0", "1", "2", "3", "4", "5", "6", "7" }));
+
+        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(phpStanEnabledCheckBox)
+                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+            .addGroup(layout.createSequentialGroup()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(phpStanConfigurationLabel)
+                    .addComponent(phpStanLevelLabel))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addGroup(layout.createSequentialGroup()
+                        .addComponent(phpStanConfigurationTextField)
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addComponent(phpStanConfigurationBrowseButton))
+                    .addGroup(layout.createSequentialGroup()
+                        .addComponent(phpStanLevelComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                        .addGap(0, 0, Short.MAX_VALUE))))
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addComponent(phpStanEnabledCheckBox)
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(phpStanConfigurationLabel)
+                    .addComponent(phpStanConfigurationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(phpStanConfigurationBrowseButton))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(phpStanLevelLabel)
+                    .addComponent(phpStanLevelComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
+        );
+    }// </editor-fold>//GEN-END:initComponents
+
+    @NbBundle.Messages("PHPStanCustomizerPanel.configuration.browse.title=Select PHPStan Configuration File")
+    private void phpStanConfigurationBrowseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_phpStanConfigurationBrowseButtonActionPerformed
+        File file = new FileChooserBuilder(PHPStanOptionsPanel.class.getName() + PHPSTAN_CONFIGURATION_LAST_FOLDER_SUFFIX)
+                .setFilesOnly(true)
+                .setTitle(Bundle.PHPStanCustomizerPanel_configuration_browse_title())
+                .showOpenDialog();
+        if (file != null) {
+            phpStanConfigurationTextField.setText(file.getAbsolutePath());
+        }
+    }//GEN-LAST:event_phpStanConfigurationBrowseButtonActionPerformed
+
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JButton phpStanConfigurationBrowseButton;
+    private javax.swing.JLabel phpStanConfigurationLabel;
+    private javax.swing.JTextField phpStanConfigurationTextField;
+    private javax.swing.JCheckBox phpStanEnabledCheckBox;
+    private javax.swing.JComboBox<String> phpStanLevelComboBox;
+    private javax.swing.JLabel phpStanLevelLabel;
+    // End of variables declaration//GEN-END:variables
+}
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/AnalysisCategoryPanel.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/AnalysisCategoryPanel.java
index fd4d737e99..5c6078876e 100644
--- a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/AnalysisCategoryPanel.java
+++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/AnalysisCategoryPanel.java
@@ -28,6 +28,8 @@
 
     public abstract void addChangeListener(ChangeListener listener);
 
+    public abstract void removeChangeListener(ChangeListener listener);
+
     public abstract void update();
 
     public abstract void applyChanges();
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 933b929dea..827fd6d827 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
@@ -40,7 +40,8 @@ private AnalysisCategoryPanels() {
         return Arrays.asList(
                 new CodeSnifferOptionsPanel(),
                 new MessDetectorOptionsPanel(),
-                new CodingStandardsFixerOptionsPanel());
+                new CodingStandardsFixerOptionsPanel(),
+                new PHPStanOptionsPanel());
     }
 
 }
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 768848ae7f..bc2482f598 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
@@ -48,3 +48,16 @@ CodingStandardsFixerOptionsPanel.codingStandardsFixerLevelLabel.text=Default &Le
 CodingStandardsFixerOptionsPanel.codingStandardsFixerOptionsLabel.text=Default &Options:
 CodingStandardsFixerOptionsPanel.codingStandardsFixerOptionsTextField.text=
 CodingStandardsFixerOptionsPanel.codingStandardsFixerVersionLabel.text=Default &Version:
+PHPStanOptionsPanel.phpStanLabel.text=&PHPStan:
+PHPStanOptionsPanel.phpStanTextField.text=
+PHPStanOptionsPanel.phpStanBrowseButton.text=&Browse...
+PHPStanOptionsPanel.phpStanSearchButton.text=&Search...
+PHPStanOptionsPanel.phpStanHintLabel.text=HINT
+PHPStanOptionsPanel.phpStanLevelLabel.text=&Level:
+PHPStanOptionsPanel.phpStanConfigurationLabel.text=&Configuration:
+PHPStanOptionsPanel.phpStanConfigurationTextField.text=
+PHPStanOptionsPanel.phpStanConfiturationBrowseButton.text=B&rowse...
+PHPStanOptionsPanel.phpStanNoteLabel.text=<html><i>Note:</i></html>
+PHPStanOptionsPanel.phpStanMinVersionInfoLabel.text=PHPStan 0.10.3 or newer is supported.
+PHPStanOptionsPanel.phpStanLearnMoreLabel.text=<html><a href="#">Learn more about PHPStan</a></html>
+PHPStanOptionsPanel.jLabel1.text=Full configuration file path(typically, phpstan.neon or phpstan.neon.dist)
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PHPStanOptionsPanel.form b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PHPStanOptionsPanel.form
new file mode 100644
index 0000000000..9894d942a4
--- /dev/null
+++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PHPStanOptionsPanel.form
@@ -0,0 +1,248 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!--
+
+    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.
+
+-->
+
+<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" attributes="0">
+              <Component id="phpStanNoteLabel" min="-2" max="-2" attributes="0"/>
+              <EmptySpace min="0" pref="0" max="32767" attributes="0"/>
+          </Group>
+          <Group type="102" attributes="0">
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="phpStanMinVersionInfoLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanLearnMoreLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="32767" attributes="0"/>
+          </Group>
+          <Group type="102" alignment="0" attributes="0">
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="phpStanConfigurationLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanLevelLabel" alignment="0" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Group type="102" attributes="0">
+                      <Group type="103" groupAlignment="1" attributes="0">
+                          <Component id="phpStanTextField" alignment="0" max="32767" attributes="0"/>
+                          <Group type="102" alignment="0" attributes="0">
+                              <Component id="phpStanHintLabel" min="-2" max="-2" attributes="0"/>
+                              <EmptySpace min="0" pref="0" max="32767" attributes="0"/>
+                          </Group>
+                      </Group>
+                      <EmptySpace max="-2" attributes="0"/>
+                      <Component id="phpStanBrowseButton" min="-2" max="-2" attributes="0"/>
+                      <EmptySpace max="-2" attributes="0"/>
+                      <Component id="phpStanSearchButton" min="-2" max="-2" attributes="0"/>
+                  </Group>
+                  <Group type="102" attributes="0">
+                      <Component id="phpStanConfigurationTextField" max="32767" attributes="0"/>
+                      <EmptySpace max="-2" attributes="0"/>
+                      <Component id="phpStanConfiturationBrowseButton" min="-2" max="-2" attributes="0"/>
+                  </Group>
+                  <Group type="102" attributes="0">
+                      <Group type="103" groupAlignment="0" attributes="0">
+                          <Component id="jLabel1" min="-2" max="-2" attributes="0"/>
+                          <Component id="phpStanLevelComboBox" min="-2" max="-2" attributes="0"/>
+                      </Group>
+                      <EmptySpace min="0" pref="0" max="32767" attributes="0"/>
+                  </Group>
+              </Group>
+          </Group>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" alignment="0" attributes="0">
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="phpStanLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanTextField" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanBrowseButton" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanSearchButton" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="-2" attributes="0"/>
+              <Component id="phpStanHintLabel" min="-2" max="-2" attributes="0"/>
+              <EmptySpace min="-2" pref="6" max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="phpStanConfigurationLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanConfigurationTextField" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanConfiturationBrowseButton" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="-2" attributes="0"/>
+              <Component id="jLabel1" min="-2" max="-2" attributes="0"/>
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="3" attributes="0">
+                  <Component id="phpStanLevelLabel" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="phpStanLevelComboBox" alignment="3" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace type="separate" max="-2" attributes="0"/>
+              <Component id="phpStanNoteLabel" min="-2" max="-2" attributes="0"/>
+              <EmptySpace max="-2" attributes="0"/>
+              <Component id="phpStanMinVersionInfoLabel" min="-2" max="-2" attributes="0"/>
+              <EmptySpace max="-2" attributes="0"/>
+              <Component id="phpStanLearnMoreLabel" min="-2" max="-2" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Component class="javax.swing.JLabel" name="phpStanLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JTextField" name="phpStanTextField">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanTextField.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JButton" name="phpStanBrowseButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanBrowseButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="phpStanBrowseButtonActionPerformed"/>
+      </Events>
+    </Component>
+    <Component class="javax.swing.JButton" name="phpStanSearchButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanSearchButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="phpStanSearchButtonActionPerformed"/>
+      </Events>
+    </Component>
+    <Component class="javax.swing.JLabel" name="phpStanHintLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanHintLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JLabel" name="phpStanLevelLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanLevelLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JComboBox" name="phpStanLevelComboBox">
+      <Properties>
+        <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor">
+          <StringArray count="8">
+            <StringItem index="0" value="0"/>
+            <StringItem index="1" value="1"/>
+            <StringItem index="2" value="2"/>
+            <StringItem index="3" value="3"/>
+            <StringItem index="4" value="4"/>
+            <StringItem index="5" value="5"/>
+            <StringItem index="6" value="6"/>
+            <StringItem index="7" value="7"/>
+          </StringArray>
+        </Property>
+      </Properties>
+      <AuxValues>
+        <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="&lt;String&gt;"/>
+      </AuxValues>
+    </Component>
+    <Component class="javax.swing.JLabel" name="phpStanConfigurationLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanConfigurationLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JTextField" name="phpStanConfigurationTextField">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanConfigurationTextField.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JButton" name="phpStanConfiturationBrowseButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanConfiturationBrowseButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="phpStanConfiturationBrowseButtonActionPerformed"/>
+      </Events>
+    </Component>
+    <Component class="javax.swing.JLabel" name="phpStanNoteLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanNoteLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JLabel" name="phpStanMinVersionInfoLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanMinVersionInfoLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JLabel" name="phpStanLearnMoreLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.phpStanLearnMoreLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="mousePressed" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="phpStanLearnMoreLabelMousePressed"/>
+        <EventHandler event="mouseEntered" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="phpStanLearnMoreLabelMouseEntered"/>
+      </Events>
+    </Component>
+    <Component class="javax.swing.JLabel" name="jLabel1">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/netbeans/modules/php/analysis/ui/options/Bundle.properties" key="PHPStanOptionsPanel.jLabel1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+  </SubComponents>
+</Form>
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PHPStanOptionsPanel.java b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PHPStanOptionsPanel.java
new file mode 100644
index 0000000000..d1afcaceae
--- /dev/null
+++ b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/options/PHPStanOptionsPanel.java
@@ -0,0 +1,415 @@
+/*
+ * 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.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import org.netbeans.modules.php.analysis.commands.PHPStan;
+import org.netbeans.modules.php.analysis.options.AnalysisOptions;
+import org.netbeans.modules.php.analysis.options.AnalysisOptionsValidator;
+import org.netbeans.modules.php.api.util.FileUtils;
+import org.netbeans.modules.php.api.util.UiUtils;
+import org.netbeans.modules.php.api.validation.ValidationResult;
+import org.openide.awt.HtmlBrowser;
+import org.openide.filesystems.FileChooserBuilder;
+import org.openide.util.ChangeSupport;
+import org.openide.util.Exceptions;
+import org.openide.util.NbBundle;
+
+public class PHPStanOptionsPanel extends AnalysisCategoryPanel {
+
+    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 long serialVersionUID = -968090640401936313L;
+
+    private final ChangeSupport changeSupport = new ChangeSupport(this);
+
+    /**
+     * Creates new form PHPStanOptionsPanel
+     */
+    public PHPStanOptionsPanel() {
+        super();
+        initComponents();
+        init();
+    }
+
+    @NbBundle.Messages({
+        "# {0} - short script name",
+        "# {1} - long script name",
+        "PHPStanOptionsPanel.hint=Full path of PHPStan script (typically {0} or {1}).",})
+    private void init() {
+        phpStanHintLabel.setText(Bundle.PHPStanOptionsPanel_hint(PHPStan.NAME, PHPStan.LONG_NAME));
+        // add listener
+        DefaultDocumentListener defaultDocumentListener = new DefaultDocumentListener();
+        phpStanTextField.getDocument().addDocumentListener(defaultDocumentListener);
+        phpStanConfigurationTextField.getDocument().addDocumentListener(defaultDocumentListener);
+        phpStanLevelComboBox.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")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        phpStanLabel = new javax.swing.JLabel();
+        phpStanTextField = new javax.swing.JTextField();
+        phpStanBrowseButton = new javax.swing.JButton();
+        phpStanSearchButton = new javax.swing.JButton();
+        phpStanHintLabel = new javax.swing.JLabel();
+        phpStanLevelLabel = new javax.swing.JLabel();
+        phpStanLevelComboBox = new javax.swing.JComboBox<>();
+        phpStanConfigurationLabel = new javax.swing.JLabel();
+        phpStanConfigurationTextField = new javax.swing.JTextField();
+        phpStanConfiturationBrowseButton = new javax.swing.JButton();
+        phpStanNoteLabel = new javax.swing.JLabel();
+        phpStanMinVersionInfoLabel = new javax.swing.JLabel();
+        phpStanLearnMoreLabel = new javax.swing.JLabel();
+        jLabel1 = new javax.swing.JLabel();
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanLabel, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanLabel.text")); // NOI18N
+
+        phpStanTextField.setText(org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanTextField.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanBrowseButton, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanBrowseButton.text")); // NOI18N
+        phpStanBrowseButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                phpStanBrowseButtonActionPerformed(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanSearchButton, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanSearchButton.text")); // NOI18N
+        phpStanSearchButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                phpStanSearchButtonActionPerformed(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanHintLabel, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanHintLabel.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanLevelLabel, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanLevelLabel.text")); // NOI18N
+
+        phpStanLevelComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "0", "1", "2", "3", "4", "5", "6", "7" }));
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanConfigurationLabel, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanConfigurationLabel.text")); // NOI18N
+
+        phpStanConfigurationTextField.setText(org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanConfigurationTextField.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanConfiturationBrowseButton, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanConfiturationBrowseButton.text")); // NOI18N
+        phpStanConfiturationBrowseButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                phpStanConfiturationBrowseButtonActionPerformed(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanNoteLabel, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanNoteLabel.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanMinVersionInfoLabel, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanMinVersionInfoLabel.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(phpStanLearnMoreLabel, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.phpStanLearnMoreLabel.text")); // NOI18N
+        phpStanLearnMoreLabel.addMouseListener(new java.awt.event.MouseAdapter() {
+            public void mousePressed(java.awt.event.MouseEvent evt) {
+                phpStanLearnMoreLabelMousePressed(evt);
+            }
+            public void mouseEntered(java.awt.event.MouseEvent evt) {
+                phpStanLearnMoreLabelMouseEntered(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(PHPStanOptionsPanel.class, "PHPStanOptionsPanel.jLabel1.text")); // NOI18N
+
+        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(phpStanNoteLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                .addGap(0, 0, Short.MAX_VALUE))
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(phpStanMinVersionInfoLabel)
+                    .addComponent(phpStanLearnMoreLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+            .addGroup(layout.createSequentialGroup()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(phpStanConfigurationLabel)
+                    .addComponent(phpStanLabel)
+                    .addComponent(phpStanLevelLabel))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addGroup(layout.createSequentialGroup()
+                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
+                            .addComponent(phpStanTextField, javax.swing.GroupLayout.Alignment.LEADING)
+                            .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
+                                .addComponent(phpStanHintLabel)
+                                .addGap(0, 0, Short.MAX_VALUE)))
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addComponent(phpStanBrowseButton)
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addComponent(phpStanSearchButton))
+                    .addGroup(layout.createSequentialGroup()
+                        .addComponent(phpStanConfigurationTextField)
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addComponent(phpStanConfiturationBrowseButton))
+                    .addGroup(layout.createSequentialGroup()
+                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                            .addComponent(jLabel1)
+                            .addComponent(phpStanLevelComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+                        .addGap(0, 0, Short.MAX_VALUE))))
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(phpStanLabel)
+                    .addComponent(phpStanTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(phpStanBrowseButton)
+                    .addComponent(phpStanSearchButton))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addComponent(phpStanHintLabel)
+                .addGap(6, 6, 6)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(phpStanConfigurationLabel)
+                    .addComponent(phpStanConfigurationTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(phpStanConfiturationBrowseButton))
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addComponent(jLabel1)
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(phpStanLevelLabel)
+                    .addComponent(phpStanLevelComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+                .addGap(18, 18, 18)
+                .addComponent(phpStanNoteLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addComponent(phpStanMinVersionInfoLabel)
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addComponent(phpStanLearnMoreLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+        );
+    }// </editor-fold>//GEN-END:initComponents
+
+    @NbBundle.Messages("PHPStanOptionsPanel.browse.title=Select PHPStan")
+    private void phpStanBrowseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_phpStanBrowseButtonActionPerformed
+        File file = new FileChooserBuilder(PHPStanOptionsPanel.class.getName() + PHPSTAN_LAST_FOLDER_SUFFIX)
+                .setFilesOnly(true)
+                .setTitle(Bundle.PHPStanOptionsPanel_browse_title())
+                .showOpenDialog();
+        if (file != null) {
+            phpStanTextField.setText(file.getAbsolutePath());
+        }
+    }//GEN-LAST:event_phpStanBrowseButtonActionPerformed
+
+    @NbBundle.Messages({
+        "PHPStanOptionsPanel.search.title=PHPStan scripts",
+        "PHPStanOptionsPanel.search.scripts=PHPStan scripts:",
+        "PHPStanOptionsPanel.search.pleaseWaitPart=PHPStan scripts",
+        "PHPStanOptionsPanel.search.notFound=No PHPStan scripts found."
+    })
+    private void phpStanSearchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_phpStanSearchButtonActionPerformed
+        String phpStan = UiUtils.SearchWindow.search(new UiUtils.SearchWindow.SearchWindowSupport() {
+
+            @Override
+            public List<String> detect() {
+                return FileUtils.findFileOnUsersPath(PHPStan.NAME, PHPStan.LONG_NAME);
+            }
+
+            @Override
+            public String getWindowTitle() {
+                return Bundle.PHPStanOptionsPanel_search_title();
+            }
+
+            @Override
+            public String getListTitle() {
+                return Bundle.PHPStanOptionsPanel_search_scripts();
+            }
+
+            @Override
+            public String getPleaseWaitPart() {
+                return Bundle.PHPStanOptionsPanel_search_pleaseWaitPart();
+            }
+
+            @Override
+            public String getNoItemsFound() {
+                return Bundle.PHPStanOptionsPanel_search_notFound();
+            }
+        });
+        if (phpStan != null) {
+            phpStanTextField.setText(phpStan);
+        }
+    }//GEN-LAST:event_phpStanSearchButtonActionPerformed
+
+    private void phpStanLearnMoreLabelMouseEntered(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_phpStanLearnMoreLabelMouseEntered
+        evt.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+    }//GEN-LAST:event_phpStanLearnMoreLabelMouseEntered
+
+    private void phpStanLearnMoreLabelMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_phpStanLearnMoreLabelMousePressed
+        try {
+            URL url = new URL("https://github.com/phpstan/phpstan"); // NOI18N
+            HtmlBrowser.URLDisplayer.getDefault().showURL(url);
+        } catch (MalformedURLException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }//GEN-LAST:event_phpStanLearnMoreLabelMousePressed
+
+    @NbBundle.Messages("PHPStanOptionsPanel.configuration.browse.title=Select PHPStan Configuration File")
+    private void phpStanConfiturationBrowseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_phpStanConfiturationBrowseButtonActionPerformed
+        File file = new FileChooserBuilder(PHPStanOptionsPanel.class.getName() + PHPSTAN_CONFIGURATION_LAST_FOLDER_SUFFIX)
+                .setFilesOnly(true)
+                .setTitle(Bundle.PHPStanOptionsPanel_configuration_browse_title())
+                .showOpenDialog();
+        if (file != null) {
+            phpStanConfigurationTextField.setText(file.getAbsolutePath());
+        }
+    }//GEN-LAST:event_phpStanConfiturationBrowseButtonActionPerformed
+
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JLabel jLabel1;
+    private javax.swing.JButton phpStanBrowseButton;
+    private javax.swing.JLabel phpStanConfigurationLabel;
+    private javax.swing.JTextField phpStanConfigurationTextField;
+    private javax.swing.JButton phpStanConfiturationBrowseButton;
+    private javax.swing.JLabel phpStanHintLabel;
+    private javax.swing.JLabel phpStanLabel;
+    private javax.swing.JLabel phpStanLearnMoreLabel;
+    private javax.swing.JComboBox<String> phpStanLevelComboBox;
+    private javax.swing.JLabel phpStanLevelLabel;
+    private javax.swing.JLabel phpStanMinVersionInfoLabel;
+    private javax.swing.JLabel phpStanNoteLabel;
+    private javax.swing.JButton phpStanSearchButton;
+    private javax.swing.JTextField phpStanTextField;
+    // End of variables declaration//GEN-END:variables
+
+    @NbBundle.Messages("PHPStanOptionsPanel.category.name=PHPStan")
+    @Override
+    public String getCategoryName() {
+        return Bundle.PHPStanOptionsPanel_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();
+        setPHPStanPath(options.getPHPStanPath());
+        setPHPStanConfigurationPath(options.getPHPStanConfigurationPath());
+        setPHPStanLevel(options.getPHPStanLevel());
+    }
+
+    @Override
+    public void applyChanges() {
+        AnalysisOptions options = AnalysisOptions.getInstance();
+        options.setPHPStanPath(getPHPStanPath());
+        options.setPHPStanConfigurationPath(getPHPStanConfigurationPath());
+        options.setPHPStanLevel(getPHPStanLevel());
+    }
+
+    @Override
+    public boolean isChanged() {
+        String saved = AnalysisOptions.getInstance().getPHPStanPath();
+        String current = getPHPStanPath();
+        if (saved == null ? !current.isEmpty() : !saved.equals(current)) {
+            return true;
+        }
+        saved = AnalysisOptions.getInstance().getPHPStanConfigurationPath();
+        current = getPHPStanConfigurationPath();
+        if (saved == null ? !current.isEmpty() : !saved.equals(current)) {
+            return true;
+        }
+        int savedInt = AnalysisOptions.getInstance().getPHPStanLevel();
+        int currentInt = getPHPStanLevel();
+        return savedInt != currentInt;
+    }
+
+    @Override
+    public ValidationResult getValidationResult() {
+        return new AnalysisOptionsValidator()
+                .validatePHPStan(getPHPStanPath(), getPHPStanConfigurationPath())
+                .getResult();
+    }
+
+    void fireChange() {
+        changeSupport.fireChange();
+    }
+
+    public String getPHPStanPath() {
+        return phpStanTextField.getText().trim();
+    }
+
+    private void setPHPStanPath(String path) {
+        phpStanTextField.setText(path);
+    }
+
+    public String getPHPStanConfigurationPath() {
+        return phpStanConfigurationTextField.getText().trim();
+    }
+
+    private void setPHPStanConfigurationPath(String path) {
+        phpStanConfigurationTextField.setText(path);
+    }
+
+    public int getPHPStanLevel() {
+        return Integer.parseInt((String) phpStanLevelComboBox.getSelectedItem());
+    }
+
+    private void setPHPStanLevel(int level) {
+        phpStanLevelComboBox.setSelectedItem(String.valueOf(level));
+    }
+
+    //~ Inner classes
+    private final class DefaultDocumentListener implements DocumentListener {
+
+        @Override
+        public void insertUpdate(DocumentEvent e) {
+            processUpdate();
+        }
+
+        @Override
+        public void removeUpdate(DocumentEvent e) {
+            processUpdate();
+        }
+
+        @Override
+        public void changedUpdate(DocumentEvent e) {
+            processUpdate();
+        }
+
+        private void processUpdate() {
+            fireChange();
+        }
+
+    }
+}
diff --git a/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/resources/phpstan.png b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/resources/phpstan.png
new file mode 100644
index 0000000000..6fe428b794
Binary files /dev/null and b/php/php.code.analysis/src/org/netbeans/modules/php/analysis/ui/resources/phpstan.png differ
diff --git a/php/php.code.analysis/test/unit/data/phpstan/PHPStanSupport/HelloWorld.php b/php/php.code.analysis/test/unit/data/phpstan/PHPStanSupport/HelloWorld.php
new file mode 100644
index 0000000000..90c534d7f0
--- /dev/null
+++ b/php/php.code.analysis/test/unit/data/phpstan/PHPStanSupport/HelloWorld.php
@@ -0,0 +1,3 @@
+<?php
+/* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0. */
+// dummy file
diff --git a/php/php.code.analysis/test/unit/data/phpstan/PHPStanSupport/vendor/nette/php-generator/src/PhpGenerator/Traits/CommentAware.php b/php/php.code.analysis/test/unit/data/phpstan/PHPStanSupport/vendor/nette/php-generator/src/PhpGenerator/Traits/CommentAware.php
new file mode 100644
index 0000000000..90c534d7f0
--- /dev/null
+++ b/php/php.code.analysis/test/unit/data/phpstan/PHPStanSupport/vendor/nette/php-generator/src/PhpGenerator/Traits/CommentAware.php
@@ -0,0 +1,3 @@
+<?php
+/* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0. */
+// dummy file
diff --git a/php/php.code.analysis/test/unit/data/phpstan/PHPStanSupport/vendor/nikic/php-parser/test/PhpParser/Builder/ClassTest.php b/php/php.code.analysis/test/unit/data/phpstan/PHPStanSupport/vendor/nikic/php-parser/test/PhpParser/Builder/ClassTest.php
new file mode 100644
index 0000000000..90c534d7f0
--- /dev/null
+++ b/php/php.code.analysis/test/unit/data/phpstan/PHPStanSupport/vendor/nikic/php-parser/test/PhpParser/Builder/ClassTest.php
@@ -0,0 +1,3 @@
+<?php
+/* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0. */
+// dummy file
diff --git a/php/php.code.analysis/test/unit/data/phpstan/phpstan-log-with-other-output.xml b/php/php.code.analysis/test/unit/data/phpstan/phpstan-log-with-other-output.xml
new file mode 100644
index 0000000000..b5a4c8858f
--- /dev/null
+++ b/php/php.code.analysis/test/unit/data/phpstan/phpstan-log-with-other-output.xml
@@ -0,0 +1,8 @@
+Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0.
+<?xml version="1.0" encoding="UTF-8"?>
+<checkstyle>
+<file name="PHPStanSupport/HelloWorld.php">
+  <error line="5" column="1" severity="error" message="Parameter $date of method HelloWorld::sayHello() has invalid typehint type DateTimeImutable." />
+  <error line="7" column="1" severity="error" message="Call to method format() on an unknown class DateTimeImutable." />
+</file>
+</checkstyle>
diff --git a/php/php.code.analysis/test/unit/data/phpstan/phpstan-log.xml b/php/php.code.analysis/test/unit/data/phpstan/phpstan-log.xml
new file mode 100644
index 0000000000..3f26206988
--- /dev/null
+++ b/php/php.code.analysis/test/unit/data/phpstan/phpstan-log.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<checkstyle>
+<file name="PHPStanSupport/HelloWorld.php">
+  <error line="5" column="1" severity="error" message="Parameter $date of method HelloWorld::sayHello() has invalid typehint type DateTimeImutable." />
+  <error line="7" column="1" severity="error" message="Call to method format() on an unknown class DateTimeImutable." />
+</file>
+<file name="PHPStanSupport/vendor/nette/php-generator/src/PhpGenerator/Traits/CommentAware.php (in context of class Nette\PhpGenerator\ClassType)">
+  <error line="28" column="1" severity="error" message="Casting to string something that's already string." />
+</file>
+<file name="PHPStanSupport/vendor/nikic/php-parser/test/PhpParser/Builder/ClassTest.php">
+  <error line="0" column="1" severity="error" message="Class PhpParser\Builder\ClassTest was not found while trying to analyse it - autoloading is probably not configured properly." />
+</file>
+</checkstyle>
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/PHPStanReportParserTest.java
new file mode 100644
index 0000000000..cdb74f27ba
--- /dev/null
+++ b/php/php.code.analysis/test/unit/src/org/netbeans/modules/php/analysis/parsers/PHPStanReportParserTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.parsers;
+
+import java.io.File;
+import java.util.List;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.assertTrue;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.modules.php.analysis.results.Result;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+
+public class PHPStanReportParserTest extends NbTestCase {
+
+    public PHPStanReportParserTest(String name) {
+        super(name);
+    }
+
+    public void testParse() throws Exception {
+        FileObject root = getRoot("phpstan/PHPStanSupport");
+        List<Result> results = PHPStanReportParser.parse(getLogFile("phpstan-log.xml"), root);
+        assertNotNull(results);
+
+        assertEquals(4, results.size());
+        Result result = results.get(0);
+        assertEquals(FileUtil.toFile(root.getFileObject("HelloWorld.php")).getAbsolutePath(), result.getFilePath());
+        assertEquals(5, result.getLine());
+        assertEquals("error: Parameter $date of method HelloWorld::sayHello() has invalid typehint type DateTimeImutable.", result.getCategory());
+        assertEquals("Parameter $date of method HelloWorld::sayHello() has invalid typehint type DateTimeImutable.", result.getDescription());
+
+
+        result = results.get(2);
+        assertEquals(FileUtil.toFile(root.getFileObject("vendor/nette/php-generator/src/PhpGenerator/Traits/CommentAware.php")).getAbsolutePath(), result.getFilePath());
+        assertEquals(28, result.getLine());
+        assertEquals("error: Casting to string something that's already string.", result.getCategory());
+        assertEquals("Casting to string something that's already string.", result.getDescription());
+
+        result = results.get(3);
+        assertEquals(FileUtil.toFile(root.getFileObject("vendor/nikic/php-parser/test/PhpParser/Builder/ClassTest.php")).getAbsolutePath(), result.getFilePath());
+        assertEquals(1, result.getLine());
+        assertEquals("error: Class PhpParser\\Builder\\ClassTest was not found while trying to analyse it - autoloading is probably not configured properly.", result.getCategory());
+        assertEquals("Class PhpParser\\Builder\\ClassTest was not found while trying to analyse it - autoloading is probably not configured properly.", result.getDescription());
+    }
+
+    public void testParseWithOtherOutput() throws Exception {
+        FileObject root = getRoot("phpstan/PHPStanSupport");
+        List<Result> results = PHPStanReportParser.parse(getLogFile("phpstan-log-with-other-output.xml"), root);
+        assertNotNull(results);
+        assertEquals(2, results.size());
+    }
+
+    private File getLogFile(String name) throws Exception {
+        assertNotNull(name);
+        File phpstan = new File(getDataDir(), "phpstan");
+        File xmlLog = new File(phpstan, name);
+        assertTrue(xmlLog.isFile());
+        return xmlLog;
+    }
+
+    private FileObject getRoot(String name) {
+        assertNotNull(name);
+        FileObject dataDir = FileUtil.toFileObject(getDataDir());
+        return dataDir.getFileObject(name);
+    }
+
+}


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@netbeans.apache.org
For additional commands, e-mail: notifications-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists