├── .gitignore
├── .github
└── FUNDING.yml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── src
├── main
│ ├── java
│ │ └── de
│ │ │ └── espend
│ │ │ └── idea
│ │ │ └── php
│ │ │ ├── generics
│ │ │ ├── inspection
│ │ │ │ ├── utils
│ │ │ │ │ └── InspectionUtil.java
│ │ │ │ ├── ClassStringLocalInspection.java
│ │ │ │ └── PsalmLocalImmutableInspection.java
│ │ │ ├── dict
│ │ │ │ └── ParameterArrayType.java
│ │ │ ├── indexer
│ │ │ │ ├── externalizer
│ │ │ │ │ └── ObjectStreamDataExternalizer.java
│ │ │ │ ├── dict
│ │ │ │ │ └── TemplateAnnotationUsage.java
│ │ │ │ └── TemplateAnnotationIndex.java
│ │ │ ├── utils
│ │ │ │ ├── PhpElementsUtil.java
│ │ │ │ ├── PhpTypeProviderUtil.java
│ │ │ │ └── GenericsUtil.java
│ │ │ ├── CompletionNavigationProvider.java
│ │ │ └── type
│ │ │ │ └── TemplateAnnotationTypeProvider.java
│ │ │ └── quality
│ │ │ ├── psalm
│ │ │ ├── PsalmQualityToolType.java
│ │ │ ├── PsalmValidationInspection.java
│ │ │ ├── blacklist
│ │ │ │ ├── PsalmValidatorBlackList.java
│ │ │ │ └── PsalmValidatorIgnoredFilesConfigurable.java
│ │ │ ├── remote
│ │ │ │ ├── PsalmValidatorInterpreterDialog.java
│ │ │ │ ├── PsalmValidatorRemoteConfiguration.java
│ │ │ │ └── PsalmValidatorRemoteConfigurationProvider.java
│ │ │ ├── form
│ │ │ │ ├── PsalmValidatorConfigurationComboBox.java
│ │ │ │ ├── PsalmValidatorConfigurableForm.java
│ │ │ │ ├── PsalmValidatorConfigurable.java
│ │ │ │ └── PsalmValidatorQualityToolConfigurableList.java
│ │ │ ├── configuration
│ │ │ │ ├── PsalmValidatorConfigurationBaseManager.java
│ │ │ │ ├── PsalmValidatorConfigurationProvider.java
│ │ │ │ ├── PsalmValidatorProjectConfiguration.java
│ │ │ │ ├── PsalmValidatorConfigurationManager.java
│ │ │ │ └── PsalmValidatorConfiguration.java
│ │ │ └── PsalmAnnotatorQualityToolAnnotator.java
│ │ │ ├── phpstan
│ │ │ ├── PhpStanQualityToolType.java
│ │ │ ├── PhpStanFixerValidationInspection.java
│ │ │ ├── blacklist
│ │ │ │ ├── PhpStanValidatorBlackList.java
│ │ │ │ └── PhpStanValidatorIgnoredFilesConfigurable.java
│ │ │ ├── remote
│ │ │ │ ├── PhpStanValidatorInterpreterDialog.java
│ │ │ │ ├── PhpStanValidatorRemoteConfiguration.java
│ │ │ │ └── PhpStanValidatorRemoteConfigurationProvider.java
│ │ │ ├── form
│ │ │ │ ├── PhpStanValidatorConfigurationComboBox.java
│ │ │ │ ├── PhpStanValidatorConfigurableForm.java
│ │ │ │ ├── PhpStanValidatorConfigurable.java
│ │ │ │ └── PhpStanValidatorQualityToolConfigurableList.java
│ │ │ ├── configuration
│ │ │ │ ├── PhpStanValidatorConfigurationBaseManager.java
│ │ │ │ ├── PhpStanValidatorConfigurationProvider.java
│ │ │ │ ├── PhpStanValidatorProjectConfiguration.java
│ │ │ │ ├── PhpStanValidatorConfigurationManager.java
│ │ │ │ └── PhpStanValidatorConfiguration.java
│ │ │ └── PhpStanAnnotatorQualityToolAnnotator.java
│ │ │ ├── QualityToolUtil.java
│ │ │ └── CheckstyleQualityToolMessageProcessor.java
│ └── resources
│ │ ├── inspectionDescriptions
│ │ ├── PsalmLocalImmutableInspection.html
│ │ ├── ClassStringLocalInspection.html
│ │ ├── PhpStanFixerValidation.html
│ │ └── PsalmValidation.html
│ │ └── META-INF
│ │ ├── phpstorm-remote-interpreter.xml
│ │ ├── change-notes.html
│ │ └── plugin.xml
└── test
│ └── java
│ └── de
│ └── espend
│ └── idea
│ └── php
│ └── generics
│ └── tests
│ ├── fixtures
│ └── CompletionNavigationProvider.php
│ ├── inspection
│ ├── fixtures
│ │ └── PsalmLocalImmutableInspection.php
│ └── PsalmLocalImmutableInspectionTest.java
│ ├── utils
│ ├── fixtures
│ │ └── fixtures.php
│ └── GenericsUtilTest.java
│ ├── CompletionNavigationProviderTest.java
│ ├── type
│ ├── fixtures
│ │ └── classes.php
│ └── TemplateAnnotationTypeProviderTest.java
│ └── indexer
│ ├── fixtures
│ └── classes.php
│ └── TemplateAnnotationIndexTest.java
├── idea-php-generics-plugin.iml
├── prepare-release.sh
├── .travis.yml
├── LICENSE
├── MAINTENANCE.md
├── CHANGELOG.md
├── gradlew.bat
├── CODE_OF_CONDUCT.md
├── README.md
└── gradlew
/.gitignore:
--------------------------------------------------------------------------------
1 | out/
2 | /.idea
3 | /php-annotation.jar
4 | /build
5 | /.gradle
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [Haehnchen]
2 | custom: https://www.paypal.me/DanielEspendiller
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Haehnchen/idea-php-generics-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ideaVersion = IU-2020.1
2 | phpPluginVersion = 201.6668.153
3 | phpRemoteInterpreter= 201.6668.60
4 | annotationPluginVersion = 5.3
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/inspection/utils/InspectionUtil.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.inspection.utils;
2 |
3 | public class InspectionUtil {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/resources/inspectionDescriptions/PsalmLocalImmutableInspection.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Check property assign "$this->a = 'test'" of a readonly property
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/main/resources/inspectionDescriptions/ClassStringLocalInspection.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Detects psalm class-string issues.
4 |
5 | Psalm class-string
6 |
7 |
--------------------------------------------------------------------------------
/idea-php-generics-plugin.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Apr 25 12:52:18 CEST 2020
2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
3 | distributionBase=GRADLE_USER_HOME
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/prepare-release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | echo "" > change-notes.html
3 | git log `git describe --tags --abbrev=0`..HEAD --no-merges --oneline --pretty=format:"- %h %s (%an)
" >> change-notes.html
4 | echo "
" >> change-notes.html
5 |
6 | cp change-notes.html src/main/resources/META-INF/
7 |
8 | rm change-notes.html
9 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/phpstorm-remote-interpreter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/PsalmQualityToolType.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm;
2 |
3 | import com.jetbrains.php.tools.quality.QualityToolType;
4 | import org.jetbrains.annotations.NotNull;
5 |
6 | /**
7 | * @author Daniel Espendiller
8 | */
9 | public class PsalmQualityToolType extends QualityToolType {
10 | @NotNull
11 | @Override
12 | public String getDisplayName() {
13 | return "Psalm";
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/test/java/de/espend/idea/php/generics/tests/fixtures/CompletionNavigationProvider.php:
--------------------------------------------------------------------------------
1 |
8 | */
9 | public class PhpStanQualityToolType extends QualityToolType {
10 | @NotNull
11 | @Override
12 | public String getDisplayName() {
13 | return "PHPStan";
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/change-notes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | - Support full class name resolving based on namespace, alias and use statement for "@template" class definition (Daniel Espendiller)
4 | - Handle IndexOutOfBoundsException when there are no parameters (Simon Podlipsky)
5 | - Force use of Java 11 (Simon Podlipsky)
6 | - Fix impossible split in TemplateAnnotationTypeProvider::complete() (Simon Podlipsky)
7 | - Move Generics inspections group under PHP (Simon Podlipsky)
8 | - Support template extraction also for "as object" (Daniel Espendiller)
9 | - Provide a first working return type resolving for @template and @extends docblocks (Daniel Espendiller)
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/main/resources/inspectionDescriptions/PhpStanFixerValidation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Reports static code problems detected by PHPStan via PHPStan executable file
4 |
5 |
6 | The inspection requires PHPStan to be properly installed and set up in the IDE under Settings/Preferences | Languages & Frameworks | PHP | Quality Tools | PHPStan.
7 | It is recommended to install PHPStan either as a Composer dependency or globally.
8 |
9 | PHPStan needs to be configured fully via phpstan.neon, also with a level so that .../phpstan analyse YOUR_FILE is working:
10 |
11 |
12 |
13 | # phpstan.neon
14 | parameters:
15 | level: 2
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/PsalmValidationInspection.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm;
2 |
3 | import com.intellij.codeInspection.CleanupLocalInspectionTool;
4 | import com.jetbrains.php.tools.quality.QualityToolAnnotator;
5 | import com.jetbrains.php.tools.quality.QualityToolValidationInspection;
6 | import org.jetbrains.annotations.NotNull;
7 |
8 | /**
9 | * @author Daniel Espendiller
10 | */
11 | public class PsalmValidationInspection extends QualityToolValidationInspection implements CleanupLocalInspectionTool {
12 | @NotNull
13 | @Override
14 | protected QualityToolAnnotator getAnnotator() {
15 | return PsalmAnnotatorQualityToolAnnotator.INSTANCE;
16 | }
17 |
18 | @Override
19 | public String getToolName() {
20 | return "Psalm";
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/blacklist/PsalmValidatorBlackList.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.blacklist;
2 |
3 | import com.intellij.openapi.components.ServiceManager;
4 | import com.intellij.openapi.components.State;
5 | import com.intellij.openapi.components.Storage;
6 | import com.intellij.openapi.project.Project;
7 | import com.jetbrains.php.tools.quality.QualityToolBlackList;
8 |
9 | /**
10 | * @author Daniel Espendiller
11 | */
12 | @State(
13 | name = "PsalmValidatorDetectorBlackList",
14 | storages = {@Storage("$WORKSPACE_FILE$")}
15 | )
16 | public class PsalmValidatorBlackList extends QualityToolBlackList {
17 | public static PsalmValidatorBlackList getInstance(Project project) {
18 | return ServiceManager.getService(project, PsalmValidatorBlackList.class);
19 | }
20 | }
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/PhpStanFixerValidationInspection.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan;
2 |
3 | import com.intellij.codeInspection.CleanupLocalInspectionTool;
4 | import com.jetbrains.php.tools.quality.QualityToolAnnotator;
5 | import com.jetbrains.php.tools.quality.QualityToolValidationInspection;
6 | import org.jetbrains.annotations.NotNull;
7 |
8 | /**
9 | * @author Daniel Espendiller
10 | */
11 | public class PhpStanFixerValidationInspection extends QualityToolValidationInspection implements CleanupLocalInspectionTool {
12 | @NotNull
13 | @Override
14 | protected QualityToolAnnotator getAnnotator() {
15 | return PhpStanAnnotatorQualityToolAnnotator.INSTANCE;
16 | }
17 |
18 | @Override
19 | public String getToolName() {
20 | return "PHPStan";
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/blacklist/PhpStanValidatorBlackList.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.blacklist;
2 |
3 | import com.intellij.openapi.components.ServiceManager;
4 | import com.intellij.openapi.components.State;
5 | import com.intellij.openapi.components.Storage;
6 | import com.intellij.openapi.project.Project;
7 | import com.jetbrains.php.tools.quality.QualityToolBlackList;
8 |
9 | /**
10 | * @author Daniel Espendiller
11 | */
12 | @State(
13 | name = "PhpStanValidatorDetectorBlackList",
14 | storages = {@Storage("$WORKSPACE_FILE$")}
15 | )
16 | public class PhpStanValidatorBlackList extends QualityToolBlackList {
17 | public static PhpStanValidatorBlackList getInstance(Project project) {
18 | return ServiceManager.getService(project, PhpStanValidatorBlackList.class);
19 | }
20 | }
--------------------------------------------------------------------------------
/src/test/java/de/espend/idea/php/generics/tests/utils/fixtures/fixtures.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | public class PsalmValidatorIgnoredFilesConfigurable extends QualityToolsIgnoreFilesConfigurable {
11 | public PsalmValidatorIgnoredFilesConfigurable(Project project) {
12 | super(PsalmValidatorBlackList.getInstance(project), project);
13 | }
14 |
15 | @NotNull
16 | public String getId() {
17 | return PsalmValidatorIgnoredFilesConfigurable.class.getName();
18 | }
19 |
20 | @NotNull
21 | protected String getQualityToolName() {
22 | return "Psalm Validator";
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/blacklist/PhpStanValidatorIgnoredFilesConfigurable.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.blacklist;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.jetbrains.php.tools.quality.QualityToolsIgnoreFilesConfigurable;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | /**
8 | * @author Daniel Espendiller
9 | */
10 | public class PhpStanValidatorIgnoredFilesConfigurable extends QualityToolsIgnoreFilesConfigurable {
11 | public PhpStanValidatorIgnoredFilesConfigurable(Project project) {
12 | super(PhpStanValidatorBlackList.getInstance(project), project);
13 | }
14 |
15 | @NotNull
16 | public String getId() {
17 | return PhpStanValidatorIgnoredFilesConfigurable.class.getName();
18 | }
19 |
20 | @NotNull
21 | protected String getQualityToolName() {
22 | return "PhpStan Validator";
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: java
3 | jdk:
4 | - openjdk11
5 |
6 | before_cache:
7 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
8 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
9 | - rm -fr $HOME/.gradle/caches/*/fileHashes/fileHashes.bin
10 | - rm -fr $HOME/.gradle/caches/*/fileHashes/fileHashes.lock
11 |
12 | cache:
13 | directories:
14 | - $HOME/.gradle/caches/
15 | - $HOME/.gradle/wrapper/
16 |
17 | before_install:
18 | - "export ORG_GRADLE_PROJECT_ideaVersion=${IDEA_VERSION}"
19 | - "export ORG_GRADLE_PROJECT_phpPluginVersion=${PHP_PLUGIN_VERSION}"
20 | - "export ORG_GRADLE_PROJECT_annotationPluginVersion=${ANNOTATION_PLUGIN_VERSION}"
21 | - "export ORG_GRADLE_PROJECT_phpRemoteInterpreter=${PHP_REMOTE_INTERPRETER}"
22 |
23 | env:
24 | - IDEA_VERSION="IU-2020.1" PHP_PLUGIN_VERSION="201.6668.153" PHP_REMOTE_INTERPRETER="201.6668.60" ANNOTATION_PLUGIN_VERSION="5.3"
25 |
26 | script:
27 | - "./gradlew check verifyPlugin buildPlugin"
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Daniel Espendiller
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/dict/ParameterArrayType.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.dict;
2 |
3 | import com.intellij.psi.PsiElement;
4 | import org.jetbrains.annotations.NotNull;
5 |
6 | import java.util.Collection;
7 |
8 | public class ParameterArrayType {
9 | final private boolean isOptional;
10 |
11 | @NotNull
12 | final private PsiElement context;
13 | final private String key;
14 |
15 | @NotNull
16 | final private Collection values;
17 |
18 | public ParameterArrayType(@NotNull String key, @NotNull Collection values, boolean isOptional, @NotNull PsiElement context) {
19 | this.key = key;
20 | this.values = values;
21 | this.isOptional = isOptional;
22 | this.context = context;
23 | }
24 |
25 | public boolean isOptional() {
26 | return isOptional;
27 | }
28 |
29 | public String getKey() {
30 | return key;
31 | }
32 |
33 | @NotNull
34 | public Collection getValues() {
35 | return values;
36 | }
37 |
38 |
39 | @NotNull
40 | public PsiElement getContext() {
41 | return context;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/remote/PsalmValidatorInterpreterDialog.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.remote;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.jetbrains.php.remote.tools.quality.QualityToolByInterpreterDialog;
5 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfiguration;
6 | import org.jetbrains.annotations.NotNull;
7 | import org.jetbrains.annotations.Nullable;
8 |
9 | import java.util.List;
10 |
11 | /**
12 | * @author Daniel Espendiller
13 | */
14 | public class PsalmValidatorInterpreterDialog extends QualityToolByInterpreterDialog {
15 | protected PsalmValidatorInterpreterDialog(@Nullable Project project, @NotNull List settings) {
16 | super(project, settings, "Psalm");
17 | }
18 |
19 | protected boolean canProcessSetting(@NotNull PsalmValidatorConfiguration settings) {
20 | return settings instanceof PsalmValidatorRemoteConfiguration;
21 | }
22 |
23 | @Nullable
24 | protected String getInterpreterId(@NotNull PsalmValidatorRemoteConfiguration configuration) {
25 | return configuration.getInterpreterId();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/remote/PhpStanValidatorInterpreterDialog.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.remote;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.jetbrains.php.remote.tools.quality.QualityToolByInterpreterDialog;
5 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfiguration;
6 | import org.jetbrains.annotations.NotNull;
7 | import org.jetbrains.annotations.Nullable;
8 |
9 | import java.util.List;
10 |
11 | /**
12 | * @author Daniel Espendiller
13 | */
14 | public class PhpStanValidatorInterpreterDialog extends QualityToolByInterpreterDialog {
15 | protected PhpStanValidatorInterpreterDialog(@Nullable Project project, @NotNull List settings) {
16 | super(project, settings, "PHPStan");
17 | }
18 |
19 | protected boolean canProcessSetting(@NotNull PhpStanValidatorConfiguration settings) {
20 | return settings instanceof PhpStanValidatorRemoteConfiguration;
21 | }
22 |
23 | @Nullable
24 | protected String getInterpreterId(@NotNull PhpStanValidatorRemoteConfiguration configuration) {
25 | return configuration.getInterpreterId();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/form/PhpStanValidatorConfigurationComboBox.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.form;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.jetbrains.php.tools.quality.*;
5 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfiguration;
6 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfigurationManager;
7 | import org.jetbrains.annotations.Nls;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.jetbrains.annotations.Nullable;
10 |
11 | /**
12 | * @author Daniel Espendiller
13 | */
14 | public class PhpStanValidatorConfigurationComboBox extends QualityToolConfigurationComboBox {
15 | public PhpStanValidatorConfigurationComboBox(@Nullable Project project) {
16 | super(project);
17 | }
18 |
19 | protected QualityToolConfigurableList getQualityToolConfigurableList(@NotNull Project project, @Nullable String item) {
20 | return new PhpStanValidatorQualityToolConfigurableList(project, item);
21 | }
22 |
23 | protected QualityToolConfigurationManager getConfigurationManager(@NotNull Project project) {
24 | return PhpStanValidatorConfigurationManager.getInstance(project);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/test/java/de/espend/idea/php/generics/tests/CompletionNavigationProviderTest.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.tests;
2 |
3 | import com.intellij.patterns.PlatformPatterns;
4 | import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
5 | import com.jetbrains.php.lang.psi.elements.PhpClass;
6 |
7 | /**
8 | * @author Daniel Espendiller
9 | */
10 | public class CompletionNavigationProviderTest extends AnnotationLightCodeInsightFixtureTestCase {
11 | public void setUp() throws Exception {
12 | super.setUp();
13 | myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("CompletionNavigationProvider.php"));
14 | }
15 |
16 | public String getTestDataPath() {
17 | return "src/test/java/de/espend/idea/php/generics/tests/fixtures";
18 | }
19 |
20 | public void testThatArrayProvidesPsalmTypesCompletion() {
21 | assertCompletionContains("test.php",
22 | "foobar([''])",
24 | "bar", "foo", "foobar", "foobar2", "foo--__---2FOO2122"
25 | );
26 | }
27 |
28 | public void testThatArrayProvidesPsalmTypesNavigation() {
29 | assertNavigationMatch("test.php",
30 | "foobar(['foobar'])",
32 | PlatformPatterns.psiElement(PhpDocTag.class)
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/indexer/externalizer/ObjectStreamDataExternalizer.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.indexer.externalizer;
2 |
3 | import com.intellij.util.io.DataExternalizer;
4 | import org.jetbrains.annotations.NotNull;
5 |
6 | import java.io.*;
7 |
8 | /**
9 | * @author Daniel Espendiller
10 | */
11 | public class ObjectStreamDataExternalizer implements DataExternalizer {
12 |
13 | @Override
14 | public void save(@NotNull DataOutput out, T value) throws IOException {
15 | ByteArrayOutputStream stream = new ByteArrayOutputStream();
16 | ObjectOutput output = new ObjectOutputStream(stream);
17 |
18 | output.writeObject(value);
19 |
20 | out.writeInt(stream.size());
21 | out.write(stream.toByteArray());
22 | }
23 |
24 | @Override
25 | public T read(@NotNull DataInput in) throws IOException {
26 | int bufferSize = in.readInt();
27 | byte[] buffer = new byte[bufferSize];
28 | in.readFully(buffer, 0, bufferSize);
29 |
30 | ByteArrayInputStream stream = new ByteArrayInputStream(buffer);
31 | ObjectInput input = new ObjectInputStream(stream);
32 |
33 | T object = null;
34 | try {
35 | object = (T) input.readObject();
36 | } catch (ClassNotFoundException | ClassCastException ignored) {
37 | }
38 |
39 | return object;
40 | }
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/form/PsalmValidatorConfigurationComboBox.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.form;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.jetbrains.php.tools.quality.QualityToolConfigurableList;
5 | import com.jetbrains.php.tools.quality.QualityToolConfigurationComboBox;
6 | import com.jetbrains.php.tools.quality.QualityToolConfigurationManager;
7 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfiguration;
8 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfigurationManager;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 |
12 | /**
13 | * @author Daniel Espendiller
14 | */
15 | public class PsalmValidatorConfigurationComboBox extends QualityToolConfigurationComboBox {
16 | public PsalmValidatorConfigurationComboBox(@Nullable Project project) {
17 | super(project);
18 | }
19 |
20 | protected QualityToolConfigurableList getQualityToolConfigurableList(@NotNull Project project, @Nullable String item) {
21 | return new PsalmValidatorQualityToolConfigurableList(project, item);
22 | }
23 |
24 | protected QualityToolConfigurationManager getConfigurationManager(@NotNull Project project) {
25 | return PsalmValidatorConfigurationManager.getInstance(project);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/resources/inspectionDescriptions/PsalmValidation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Reports static code problems detected by Psalm via executable file
4 |
5 |
6 | The inspection requires Psalm to be properly installed and set up in the IDE under Settings/Preferences | Languages & Frameworks | PHP | Quality Tools | Psalm.
7 | It is recommended to install Psalm either as a Composer dependency or globally.
8 |
9 | PHPStan needs to be configured fully via psalm.xml so that .../psalm YOUR_FILE is working:
10 |
11 |
12 |
13 | # psalm.xml
14 | <![CDATA[
15 | <?xml version="1.0"?>
16 | <psalm
17 | totallyTyped="true"
18 | errorLevel="1"
19 | resolveFromConfigFile="true"
20 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
21 | xmlns="https://getpsalm.org/schema/config"
22 | xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
23 | >
24 | <projectFiles>
25 | <directory name="src" />
26 | <ignoreFiles>
27 | <directory name="vendor" />
28 | </ignoreFiles>
29 | </projectFiles>
30 | </psalm>
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/form/PsalmValidatorConfigurableForm.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.form;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.openapi.util.Pair;
5 | import com.intellij.openapi.vfs.VirtualFile;
6 | import com.jetbrains.php.tools.quality.QualityToolConfigurableForm;
7 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfiguration;
8 | import org.apache.commons.lang.StringUtils;
9 | import org.jetbrains.annotations.Nls;
10 | import org.jetbrains.annotations.NotNull;
11 | import org.jetbrains.annotations.Nullable;
12 |
13 | public class PsalmValidatorConfigurableForm extends QualityToolConfigurableForm {
14 | public PsalmValidatorConfigurableForm(@NotNull Project project, @NotNull C configuration) {
15 | super(project, configuration, "Psalm", "psalm");
16 | }
17 |
18 | @Nls
19 | public String getDisplayName() {
20 | return "Psalm";
21 | }
22 |
23 | @Nullable
24 | public String getHelpTopic() {
25 | return "settings.psalm.codeStyle";
26 | }
27 |
28 | @NotNull
29 | public String getId() {
30 | return PsalmValidatorConfigurableForm.class.getName();
31 | }
32 |
33 | @NotNull
34 | public Pair validateMessage(String message) {
35 | return message.toLowerCase().contains("psalm")
36 | ? Pair.create(true, "OK, " + StringUtils.abbreviate(message, 100))
37 | : Pair.create(false, message);
38 | }
39 |
40 | public boolean isValidToolFile(VirtualFile file) {
41 | return file.getName().startsWith("psalm");
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/form/PhpStanValidatorConfigurableForm.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.form;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.openapi.util.Pair;
5 | import com.intellij.openapi.util.Version;
6 | import com.intellij.openapi.vfs.VirtualFile;
7 | import com.jetbrains.php.tools.quality.QualityToolConfigurableForm;
8 | import com.jetbrains.php.tools.quality.messDetector.MessDetectorConfigurableForm;
9 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfiguration;
10 | import org.jetbrains.annotations.Nls;
11 | import org.jetbrains.annotations.NotNull;
12 | import org.jetbrains.annotations.Nullable;
13 |
14 | public class PhpStanValidatorConfigurableForm extends QualityToolConfigurableForm {
15 | public PhpStanValidatorConfigurableForm(@NotNull Project project, @NotNull C configuration) {
16 | super(project, configuration, "PhpStan", "phpstan");
17 | }
18 |
19 | @Nls
20 | public String getDisplayName() {
21 | return "PhpStan";
22 | }
23 |
24 | @Nullable
25 | public String getHelpTopic() {
26 | return "settings.phpstan.codeStyle";
27 | }
28 |
29 | @NotNull
30 | public String getId() {
31 | return PhpStanValidatorConfigurableForm.class.getName();
32 | }
33 |
34 | @NotNull
35 | public Pair validateMessage(String message) {
36 | return message.contains("PHPStan")
37 | ? Pair.create(true, "OK, " + message)
38 | : Pair.create(false, message);
39 | }
40 |
41 | public boolean isValidToolFile(VirtualFile file) {
42 | return file.getName().startsWith("phpstan");
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/MAINTENANCE.md:
--------------------------------------------------------------------------------
1 | # Maintaining the plugin
2 |
3 | ## Forging a new release
4 |
5 | The plugin is released manually, based on a git tag.
6 |
7 | A gradle plugin automatically determines the current tag and / or if this
8 | is a snapshot release.
9 |
10 | To build the plugin, execute the gradle task `buildPlugin`.
11 |
12 | ```bash
13 | ./gradlew clean buildPlugin
14 | ```
15 |
16 | The artifact zip can then be found in `build/distrubutions`. This is the
17 | final result which can be uploaded to the JetBrains repository.
18 |
19 | The checklist for a new release should be the following:
20 |
21 | * ensure the project is currently on the latest commit on the `master` branch
22 | You can enforce this by pulling and resetting with the `--hard` flag
23 | * make sure there are no staged changes
24 | * prepare the changelog:
25 | * execute `./prepare-release.sh` to write the changelog to disk
26 | * manually copy the relevant parts to `CHANGELOG.md`
27 | * commit the changed files (preferrable with a meaningful commit message
28 | `Prepare release 0.16.xxx`)
29 | * tag a release (`git tag 0.x.xxx`)
30 | * push the changed code and the tag to the remote (`git push && git push --tags`)
31 | * build the plugin on the tag (`./gradlew clean buildPlugin`)
32 |
33 | ## Upload to JetBrain Plugin repository
34 |
35 | The plugin can be updated in two different ways.
36 |
37 | ### Manual upload
38 |
39 | Upload the produced ZIP file to the JetBrains repository manually
40 |
41 | ### Semi-automatic upload through gradle
42 |
43 | The IntelliJ gradle plugin ships a task to upload the release
44 | automatically. This will include the changelog generated earlier.
45 |
46 | Execute the following gradle task:
47 |
48 | ```bash
49 | IJ_REPO_USERNAME=youruser IJ_REPO_PASSWORD=yourpassword ./gradlew clean buildPlugin publishPlugin
50 | ```
51 |
--------------------------------------------------------------------------------
/src/test/java/de/espend/idea/php/generics/tests/type/fixtures/classes.php:
--------------------------------------------------------------------------------
1 | $class
18 | * @return T
19 | */
20 | function _barInstantiator(string $class) {
21 | return new $class();
22 | }
23 | }
24 | }
25 |
26 | namespace
27 | {
28 | /**
29 | * @template T
30 | * @psalm-param class-string $class
31 | * @return T
32 | */
33 | function instantiator(string $class) {
34 | return new $class();
35 | }
36 |
37 | /**
38 | * @template T
39 | * @psalm-param class-string $class2
40 | * @return T
41 | */
42 | function instantiator2(string $class, string $class2, string $class3) {
43 | return new $class();
44 | }
45 | }
46 |
47 |
48 | namespace Extended\Implementations
49 | {
50 | /**
51 | * @template T
52 | */
53 | class MyContainer
54 | {
55 | /** @var T */
56 | private $value;
57 |
58 | /** @param T $value */
59 | public function __construct($value) {
60 | $this->value = $value;
61 | }
62 |
63 | /** @psalm-return T */
64 | public function getValue() {
65 | return $this->value;
66 | }
67 | }
68 |
69 | class Foobar
70 | {
71 | public function getFoobar(){}
72 | }
73 | }
74 |
75 | namespace Extended\Classes
76 | {
77 | use Extended\Implementations\MyContainer;
78 |
79 | /**
80 | * @extends \Extended\Implementations\MyContainer<\Extended\Implementations\Foobar>
81 | */
82 | class MyExtendsImpl extends MyContainer
83 | {
84 | }
85 | }
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/configuration/PsalmValidatorConfigurationBaseManager.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.configuration;
2 |
3 | import com.intellij.openapi.components.ServiceManager;
4 | import com.intellij.util.xmlb.XmlSerializer;
5 | import com.jetbrains.php.tools.quality.QualityToolConfigurationBaseManager;
6 | import com.jetbrains.php.tools.quality.QualityToolConfigurationProvider;
7 | import org.jdom.Element;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.jetbrains.annotations.Nullable;
10 |
11 | /**
12 | * @author Daniel Espendiller
13 | */
14 | public class PsalmValidatorConfigurationBaseManager extends QualityToolConfigurationBaseManager {
15 | public PsalmValidatorConfigurationBaseManager() {
16 | }
17 |
18 | public static PsalmValidatorConfigurationBaseManager getInstance() {
19 | return ServiceManager.getService(PsalmValidatorConfigurationBaseManager.class);
20 | }
21 |
22 | @NotNull
23 | protected PsalmValidatorConfiguration createLocalSettings() {
24 | return new PsalmValidatorConfiguration();
25 | }
26 |
27 | @NotNull
28 | protected String getQualityToolName() {
29 | return "Psalm";
30 | }
31 |
32 | @NotNull
33 | protected String getOldStyleToolPathName() {
34 | return "psalm";
35 | }
36 |
37 | @NotNull
38 | protected String getConfigurationRootName() {
39 | return "psalm_settings";
40 | }
41 |
42 | @Nullable
43 | protected QualityToolConfigurationProvider getConfigurationProvider() {
44 | return PsalmValidatorConfigurationProvider.getInstances();
45 | }
46 |
47 | @Nullable
48 | protected PsalmValidatorConfiguration loadLocal(Element element) {
49 | return XmlSerializer.deserialize(element, PsalmValidatorConfiguration.class);
50 | }
51 | }
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/configuration/PhpStanValidatorConfigurationBaseManager.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.configuration;
2 |
3 | import com.intellij.openapi.components.ServiceManager;
4 | import com.intellij.util.xmlb.XmlSerializer;
5 | import com.jetbrains.php.tools.quality.QualityToolConfigurationBaseManager;
6 | import com.jetbrains.php.tools.quality.QualityToolConfigurationProvider;
7 | import org.jdom.Element;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.jetbrains.annotations.Nullable;
10 |
11 | /**
12 | * @author Daniel Espendiller
13 | */
14 | public class PhpStanValidatorConfigurationBaseManager extends QualityToolConfigurationBaseManager {
15 | public PhpStanValidatorConfigurationBaseManager() {
16 | }
17 |
18 | public static PhpStanValidatorConfigurationBaseManager getInstance() {
19 | return ServiceManager.getService(PhpStanValidatorConfigurationBaseManager.class);
20 | }
21 |
22 | @NotNull
23 | protected PhpStanValidatorConfiguration createLocalSettings() {
24 | return new PhpStanValidatorConfiguration();
25 | }
26 |
27 | @NotNull
28 | protected String getQualityToolName() {
29 | return "PhpStan";
30 | }
31 |
32 | @NotNull
33 | protected String getOldStyleToolPathName() {
34 | return "phpstan";
35 | }
36 |
37 | @NotNull
38 | protected String getConfigurationRootName() {
39 | return "phpstan_settings";
40 | }
41 |
42 | @Nullable
43 | protected QualityToolConfigurationProvider getConfigurationProvider() {
44 | return PhpStanValidatorConfigurationProvider.getInstances();
45 | }
46 |
47 | @Nullable
48 | protected PhpStanValidatorConfiguration loadLocal(Element element) {
49 | return XmlSerializer.deserialize(element, PhpStanValidatorConfiguration.class);
50 | }
51 | }
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/configuration/PsalmValidatorConfigurationProvider.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.configuration;
2 |
3 | import com.intellij.openapi.extensions.ExtensionPointName;
4 | import com.intellij.openapi.util.text.StringUtil;
5 | import com.intellij.util.NullableFunction;
6 | import com.jetbrains.php.tools.quality.QualityToolConfigurationProvider;
7 | import org.jetbrains.annotations.NotNull;
8 | import org.jetbrains.annotations.Nullable;
9 |
10 | /**
11 | * @author Daniel Espendiller
12 | */
13 | public abstract class PsalmValidatorConfigurationProvider extends QualityToolConfigurationProvider {
14 | private static final ExtensionPointName EP_NAME = ExtensionPointName.create("de.espend.idea.php.quality.psalm.psalmConfigurationProvider");
15 |
16 | @Nullable
17 | public static PsalmValidatorConfigurationProvider getInstances() {
18 | // make org.jetbrains.plugins.phpstorm-remote-interpreter optional; like done by PhpStorm implementations
19 | PsalmValidatorConfigurationProvider[] extensions = EP_NAME.getExtensions();
20 | if (extensions.length > 1) {
21 | throw new RuntimeException("Several providers for remote Psalm configuration was found");
22 | }
23 |
24 | return extensions.length == 1 ? extensions[0] : null;
25 | }
26 |
27 | protected void fillSettingsByDefaultValue(@NotNull PsalmValidatorConfiguration settings, @NotNull PsalmValidatorConfiguration localConfiguration, @NotNull NullableFunction preparePath) {
28 | super.fillSettingsByDefaultValue(settings, localConfiguration, preparePath);
29 |
30 | String toolPath = preparePath.fun(localConfiguration.getToolPath());
31 | if (StringUtil.isNotEmpty(toolPath)) {
32 | settings.setToolPath(toolPath);
33 | }
34 |
35 | settings.setTimeout(localConfiguration.getTimeout());
36 | }
37 | }
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/configuration/PhpStanValidatorConfigurationProvider.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.configuration;
2 |
3 | import com.intellij.openapi.extensions.ExtensionPointName;
4 | import com.intellij.openapi.util.text.StringUtil;
5 | import com.intellij.util.NullableFunction;
6 | import com.jetbrains.php.tools.quality.QualityToolConfigurationProvider;
7 | import org.jetbrains.annotations.NotNull;
8 | import org.jetbrains.annotations.Nullable;
9 |
10 | /**
11 | * @author Daniel Espendiller
12 | */
13 | public abstract class PhpStanValidatorConfigurationProvider extends QualityToolConfigurationProvider {
14 | private static final ExtensionPointName EP_NAME = ExtensionPointName.create("de.espend.idea.php.quality.phpstan.phpStanConfigurationProvider");
15 |
16 | @Nullable
17 | public static PhpStanValidatorConfigurationProvider getInstances() {
18 | // make org.jetbrains.plugins.phpstorm-remote-interpreter optional; like done by PhpStorm implementations
19 | PhpStanValidatorConfigurationProvider[] extensions = EP_NAME.getExtensions();
20 | if (extensions.length > 1) {
21 | throw new RuntimeException("Several providers for remote PhpStan configuration was found");
22 | }
23 |
24 | return extensions.length == 1 ? extensions[0] : null;
25 | }
26 |
27 | protected void fillSettingsByDefaultValue(@NotNull PhpStanValidatorConfiguration settings, @NotNull PhpStanValidatorConfiguration localConfiguration, @NotNull NullableFunction preparePath) {
28 | super.fillSettingsByDefaultValue(settings, localConfiguration, preparePath);
29 |
30 | String toolPath = preparePath.fun(localConfiguration.getToolPath());
31 | if (StringUtil.isNotEmpty(toolPath)) {
32 | settings.setToolPath(toolPath);
33 | }
34 |
35 | settings.setTimeout(localConfiguration.getTimeout());
36 | }
37 | }
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/configuration/PsalmValidatorProjectConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.configuration;
2 |
3 | import com.intellij.openapi.components.PersistentStateComponent;
4 | import com.intellij.openapi.components.ServiceManager;
5 | import com.intellij.openapi.components.State;
6 | import com.intellij.openapi.components.Storage;
7 | import com.intellij.openapi.project.Project;
8 | import com.intellij.util.xmlb.XmlSerializerUtil;
9 | import com.jetbrains.php.tools.quality.QualityToolProjectConfiguration;
10 | import de.espend.idea.php.quality.psalm.PsalmValidationInspection;
11 | import org.jetbrains.annotations.NotNull;
12 | import org.jetbrains.annotations.Nullable;
13 |
14 | /**
15 | * @author Daniel Espendiller
16 | */
17 | @State(
18 | name = "PsalmValidatorProjectConfiguration",
19 | storages = {@Storage("$WORKSPACE_FILE$")}
20 | )
21 | public class PsalmValidatorProjectConfiguration extends QualityToolProjectConfiguration implements PersistentStateComponent {
22 | public PsalmValidatorProjectConfiguration() {
23 | }
24 |
25 | public static PsalmValidatorProjectConfiguration getInstance(Project project) {
26 | return ServiceManager.getService(project, PsalmValidatorProjectConfiguration.class);
27 | }
28 |
29 | @Nullable
30 | public PsalmValidatorProjectConfiguration getState() {
31 | return this;
32 | }
33 |
34 | @Override
35 | public void loadState(@NotNull PsalmValidatorProjectConfiguration state) {
36 | XmlSerializerUtil.copyBean(state, this);
37 | }
38 |
39 | protected String getInspectionId() {
40 | return (new PsalmValidationInspection()).getID();
41 | }
42 |
43 | @NotNull
44 | protected String getQualityToolName() {
45 | return "Psalm Validator";
46 | }
47 |
48 | @NotNull
49 | protected PsalmValidatorConfigurationManager getConfigurationManager(@NotNull Project project) {
50 | return PsalmValidatorConfigurationManager.getInstance(project);
51 | }
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/configuration/PhpStanValidatorProjectConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.configuration;
2 |
3 | import com.intellij.openapi.components.PersistentStateComponent;
4 | import com.intellij.openapi.components.ServiceManager;
5 | import com.intellij.openapi.components.State;
6 | import com.intellij.openapi.components.Storage;
7 | import com.intellij.openapi.project.Project;
8 | import com.intellij.util.xmlb.XmlSerializerUtil;
9 | import com.jetbrains.php.tools.quality.QualityToolProjectConfiguration;
10 | import de.espend.idea.php.quality.phpstan.PhpStanFixerValidationInspection;
11 | import org.jetbrains.annotations.NotNull;
12 | import org.jetbrains.annotations.Nullable;
13 |
14 | /**
15 | * @author Daniel Espendiller
16 | */
17 | @State(
18 | name = "PhpStanValidatorProjectConfiguration",
19 | storages = {@Storage("$WORKSPACE_FILE$")}
20 | )
21 | public class PhpStanValidatorProjectConfiguration extends QualityToolProjectConfiguration implements PersistentStateComponent {
22 | public PhpStanValidatorProjectConfiguration() {
23 | }
24 |
25 | public static PhpStanValidatorProjectConfiguration getInstance(Project project) {
26 | return ServiceManager.getService(project, PhpStanValidatorProjectConfiguration.class);
27 | }
28 |
29 | @Nullable
30 | public PhpStanValidatorProjectConfiguration getState() {
31 | return this;
32 | }
33 |
34 | @Override
35 | public void loadState(@NotNull PhpStanValidatorProjectConfiguration state) {
36 | XmlSerializerUtil.copyBean(state, this);
37 | }
38 |
39 | protected String getInspectionId() {
40 | return (new PhpStanFixerValidationInspection()).getID();
41 | }
42 |
43 | @NotNull
44 | protected String getQualityToolName() {
45 | return "PhpStan Validator";
46 | }
47 |
48 | @NotNull
49 | protected PhpStanValidatorConfigurationManager getConfigurationManager(@NotNull Project project) {
50 | return PhpStanValidatorConfigurationManager.getInstance(project);
51 | }
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/src/test/java/de/espend/idea/php/generics/tests/inspection/PsalmLocalImmutableInspectionTest.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.tests.inspection;
2 |
3 | import de.espend.idea.php.generics.tests.AnnotationLightCodeInsightFixtureTestCase;
4 |
5 | /*
6 | * @author Daniel Espendiller
7 | */
8 | public class PsalmLocalImmutableInspectionTest extends AnnotationLightCodeInsightFixtureTestCase {
9 | public void setUp() throws Exception {
10 | super.setUp();
11 | myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("PsalmLocalImmutableInspection.php"));
12 | }
13 |
14 | public String getTestDataPath() {
15 | return "src/test/java/de/espend/idea/php/generics/tests/inspection/fixtures";
16 | }
17 |
18 | public void testThatPsalmReadOnlyAssignmentExpressionIsMarked() {
19 | assertLocalInspectionContains(
20 | "test.php",
21 | "readOnly = 'test'",
22 | "[psalm] property marked as readonly"
23 | );
24 |
25 | assertLocalInspectionIsEmpty(
26 | "test.php",
27 | "write = 'test'"
28 | );
29 |
30 | assertLocalInspectionIsEmpty(
31 | "test.php",
32 | "readOnly = 'test';\n" +
37 | " }\n" +
38 | "\n" +
39 | "};"
40 | );
41 | }
42 |
43 | public void testThatPsalmImmutableAssignmentExpressionIsMarked() {
44 | assertLocalInspectionContains(
45 | "test.php",
46 | "readOnly = 'test'",
47 | "[psalm] property marked as readonly"
48 | );
49 |
50 | assertLocalInspectionContains(
51 | "test.php",
52 | "write = 'test'",
53 | "[psalm] property marked as readonly"
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/utils/PhpElementsUtil.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.utils;
2 |
3 | import com.intellij.patterns.PlatformPatterns;
4 | import com.intellij.patterns.PsiElementPattern;
5 | import com.intellij.psi.PsiElement;
6 | import com.jetbrains.php.lang.parser.PhpElementTypes;
7 | import com.jetbrains.php.lang.psi.elements.*;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.jetbrains.annotations.Nullable;
10 |
11 | public class PhpElementsUtil {
12 | /**
13 | * Provide array key pattern. we need incomplete array key support, too.
14 | *
15 | * foo([''])
16 | * foo(['' => 'foobar'])
17 | */
18 | @NotNull
19 | public static PsiElementPattern.Capture getParameterListArrayValuePattern() {
20 | return PlatformPatterns.psiElement()
21 | .withParent(PlatformPatterns.psiElement(StringLiteralExpression.class).withParent(
22 | PlatformPatterns.or(
23 | PlatformPatterns.psiElement().withElementType(PhpElementTypes.ARRAY_VALUE)
24 | .withParent(PlatformPatterns.psiElement(ArrayCreationExpression.class)
25 | .withParent(ParameterList.class)
26 | ),
27 |
28 | PlatformPatterns.psiElement().withElementType(PhpElementTypes.ARRAY_KEY)
29 | .withParent(PlatformPatterns.psiElement(ArrayHashElement.class)
30 | .withParent(PlatformPatterns.psiElement(ArrayCreationExpression.class)
31 | .withParent(ParameterList.class)
32 | )
33 | )
34 | ))
35 | );
36 | }
37 |
38 | public static int getCurrentParameterIndex(@NotNull ParameterList parameterList, @NotNull PsiElement psiElement) {
39 | PsiElement[] parameters = parameterList.getParameters();
40 |
41 | int i;
42 | for(i = 0; i < parameters.length; i++) {
43 | if(parameters[i].equals(psiElement)) {
44 | return i;
45 | }
46 | }
47 |
48 | return -1;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/form/PsalmValidatorConfigurable.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.form;
2 |
3 | import com.intellij.openapi.options.Configurable;
4 | import com.intellij.openapi.project.Project;
5 | import com.jetbrains.php.tools.quality.QualityToolConfigurationComboBox;
6 | import com.jetbrains.php.tools.quality.QualityToolProjectConfigurableForm;
7 | import com.jetbrains.php.tools.quality.QualityToolProjectConfiguration;
8 | import com.jetbrains.php.tools.quality.QualityToolsIgnoreFilesConfigurable;
9 | import de.espend.idea.php.quality.psalm.PsalmValidationInspection;
10 | import de.espend.idea.php.quality.psalm.blacklist.PsalmValidatorIgnoredFilesConfigurable;
11 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorProjectConfiguration;
12 | import org.jetbrains.annotations.Nls;
13 | import org.jetbrains.annotations.NotNull;
14 |
15 | /**
16 | * @author Daniel Espendiller
17 | */
18 | public class PsalmValidatorConfigurable extends QualityToolProjectConfigurableForm implements Configurable.NoScroll {
19 | public PsalmValidatorConfigurable(@NotNull Project project) {
20 | super(project);
21 | }
22 |
23 | protected QualityToolProjectConfiguration getProjectConfiguration() {
24 | return PsalmValidatorProjectConfiguration.getInstance(this.myProject);
25 | }
26 |
27 | @Nls
28 | public String getDisplayName() {
29 | return "Psalm";
30 | }
31 |
32 | public String getHelpTopic() {
33 | return "settings.psalm.validator";
34 | }
35 |
36 | @NotNull
37 | public String getId() {
38 | return PsalmValidatorConfigurable.class.getName();
39 | }
40 |
41 | @NotNull
42 | protected String getInspectionShortName() {
43 | return new PsalmValidationInspection().getShortName();
44 | }
45 |
46 | @NotNull
47 | protected QualityToolConfigurationComboBox createConfigurationComboBox() {
48 | return new PsalmValidatorConfigurationComboBox(this.myProject);
49 | }
50 |
51 | protected QualityToolsIgnoreFilesConfigurable getIgnoredFilesConfigurable() {
52 | return new PsalmValidatorIgnoredFilesConfigurable(this.myProject);
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/form/PhpStanValidatorConfigurable.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.form;
2 |
3 | import com.intellij.openapi.options.Configurable;
4 | import com.intellij.openapi.project.Project;
5 | import com.jetbrains.php.tools.quality.QualityToolConfigurationComboBox;
6 | import com.jetbrains.php.tools.quality.QualityToolProjectConfigurableForm;
7 | import com.jetbrains.php.tools.quality.QualityToolProjectConfiguration;
8 | import com.jetbrains.php.tools.quality.QualityToolsIgnoreFilesConfigurable;
9 | import com.jetbrains.php.tools.quality.messDetector.*;
10 | import de.espend.idea.php.quality.phpstan.PhpStanFixerValidationInspection;
11 | import de.espend.idea.php.quality.phpstan.blacklist.PhpStanValidatorIgnoredFilesConfigurable;
12 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorProjectConfiguration;
13 | import org.jetbrains.annotations.Nls;
14 | import org.jetbrains.annotations.NotNull;
15 |
16 | /**
17 | * @author Daniel Espendiller
18 | */
19 | public class PhpStanValidatorConfigurable extends QualityToolProjectConfigurableForm implements Configurable.NoScroll {
20 | public PhpStanValidatorConfigurable(@NotNull Project project) {
21 | super(project);
22 | }
23 |
24 | protected QualityToolProjectConfiguration getProjectConfiguration() {
25 | return PhpStanValidatorProjectConfiguration.getInstance(this.myProject);
26 | }
27 |
28 | @Nls
29 | public String getDisplayName() {
30 | return "PhpStan";
31 | }
32 |
33 | public String getHelpTopic() {
34 | return "settings.phpstan.validator";
35 | }
36 |
37 | @NotNull
38 | public String getId() {
39 | return PhpStanValidatorConfigurable.class.getName();
40 | }
41 |
42 | @NotNull
43 | protected String getInspectionShortName() {
44 | return new PhpStanFixerValidationInspection().getShortName();
45 | }
46 |
47 | @NotNull
48 | protected QualityToolConfigurationComboBox createConfigurationComboBox() {
49 | return new PhpStanValidatorConfigurationComboBox(this.myProject);
50 | }
51 |
52 | protected QualityToolsIgnoreFilesConfigurable getIgnoredFilesConfigurable() {
53 | return new PhpStanValidatorIgnoredFilesConfigurable(this.myProject);
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/src/test/java/de/espend/idea/php/generics/tests/type/TemplateAnnotationTypeProviderTest.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.tests.type;
2 |
3 | import com.intellij.patterns.PlatformPatterns;
4 | import com.jetbrains.php.lang.PhpFileType;
5 | import com.jetbrains.php.lang.psi.elements.Method;
6 | import de.espend.idea.php.generics.tests.AnnotationLightCodeInsightFixtureTestCase;
7 |
8 | /**
9 | * @author Daniel Espendiller
10 | */
11 | public class TemplateAnnotationTypeProviderTest extends AnnotationLightCodeInsightFixtureTestCase {
12 | public void setUp() throws Exception {
13 | super.setUp();
14 | myFixture.copyFileToProject("classes.php");
15 | }
16 |
17 | public String getTestDataPath() {
18 | return "src/test/java/de/espend/idea/php/generics/tests/type/fixtures";
19 | }
20 |
21 | public void testTypesForFunctionWithClassString() {
22 | assertPhpReferenceResolveTo(PhpFileType.INSTANCE,
23 | "getFoo();\n",
24 | PlatformPatterns.psiElement(Method.class).withName("getFoo")
25 | );
26 |
27 | assertPhpReferenceResolveTo(PhpFileType.INSTANCE,
28 | "getFoo();\n",
29 | PlatformPatterns.psiElement(Method.class).withName("getFoo")
30 | );
31 |
32 | assertPhpReferenceNotResolveTo(PhpFileType.INSTANCE,
33 | "getFoo();\n",
34 | PlatformPatterns.psiElement(Method.class).withName("getFoo")
35 | );
36 | }
37 |
38 | public void testTypesForMethodWithClassString() {
39 | assertPhpReferenceResolveTo(PhpFileType.INSTANCE,
40 | "_barInstantiator(\\Foobar\\Foobar::class)->getFoo();\n",
41 | PlatformPatterns.psiElement(Method.class).withName("getFoo")
42 | );
43 | }
44 |
45 | public void testTypesTemplateExtends() {
46 | assertPhpReferenceResolveTo(PhpFileType.INSTANCE,
47 | "getValue()->getFoobar();\n",
48 | PlatformPatterns.psiElement(Method.class).withName("getFoobar")
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/remote/PsalmValidatorRemoteConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.remote;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.openapi.util.text.StringUtil;
5 | import com.intellij.util.xmlb.annotations.Attribute;
6 | import com.intellij.util.xmlb.annotations.Tag;
7 | import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl;
8 | import com.jetbrains.php.config.interpreters.PhpSdkDependentConfiguration;
9 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfiguration;
10 | import org.jetbrains.annotations.NotNull;
11 | import org.jetbrains.annotations.Nullable;
12 |
13 | /**
14 | * @author Daniel Espendiller
15 | */
16 | @Tag("psalm_by_interpreter")
17 | public class PsalmValidatorRemoteConfiguration extends PsalmValidatorConfiguration implements PhpSdkDependentConfiguration {
18 | private String myInterpreterId;
19 |
20 | @Attribute("interpreter_id")
21 | @Nullable
22 | public String getInterpreterId() {
23 | return this.myInterpreterId;
24 | }
25 |
26 | public void setInterpreterId(@NotNull String interpreterId) {
27 | this.myInterpreterId = interpreterId;
28 | }
29 |
30 | @NotNull
31 | public String getPresentableName(@Nullable Project project) {
32 | return getDefaultName(PhpInterpretersManagerImpl.getInstance(project).findInterpreterName(this.getInterpreterId()));
33 | }
34 |
35 | @NotNull
36 | public String getId() {
37 | String interpreterId = this.getInterpreterId();
38 | return StringUtil.isEmpty(interpreterId) ? "Undefined interpreter" : interpreterId;
39 | }
40 |
41 | @NotNull
42 | public static String getDefaultName(@Nullable String interpreterName) {
43 |
44 | return StringUtil.isEmpty(interpreterName) ? "Undefined interpreter" : "Interpreter: " + interpreterName;
45 | }
46 |
47 | public PsalmValidatorRemoteConfiguration clone() {
48 | PsalmValidatorRemoteConfiguration settings = new PsalmValidatorRemoteConfiguration();
49 | settings.myInterpreterId = this.myInterpreterId;
50 | this.clone(settings);
51 | return settings;
52 | }
53 |
54 | public String serialize(@Nullable String path) {
55 | return path;
56 | }
57 |
58 | public String deserialize(@Nullable String path) {
59 | return path;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/configuration/PsalmValidatorConfigurationManager.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.configuration;
2 |
3 | import com.intellij.openapi.components.ServiceManager;
4 | import com.intellij.openapi.components.State;
5 | import com.intellij.openapi.components.Storage;
6 | import com.intellij.openapi.project.Project;
7 | import com.intellij.openapi.project.ProjectManager;
8 | import com.jetbrains.php.tools.quality.QualityToolConfigurationManager;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 |
12 | import java.util.List;
13 |
14 | /**
15 | * @author Daniel Espendiller
16 | */
17 | public class PsalmValidatorConfigurationManager extends QualityToolConfigurationManager {
18 | public PsalmValidatorConfigurationManager(@Nullable Project project) {
19 | super(project);
20 | if (project != null) {
21 | this.myProjectManager = ServiceManager.getService(project, ProjectPsalmValidatorConfigurationBaseManager.class);
22 | }
23 |
24 | this.myApplicationManager = ServiceManager.getService(AppPsalmValidatorConfigurationBaseManager.class);
25 | }
26 |
27 | @NotNull
28 | protected List getDefaultProjectSettings() {
29 | ProjectPsalmValidatorConfigurationBaseManager service = ServiceManager.getService(
30 | ProjectManager.getInstance().getDefaultProject(),
31 | ProjectPsalmValidatorConfigurationBaseManager.class
32 | );
33 |
34 | return service.getSettings();
35 | }
36 |
37 | public static PsalmValidatorConfigurationManager getInstance(@NotNull Project project) {
38 | return ServiceManager.getService(project, PsalmValidatorConfigurationManager.class);
39 | }
40 |
41 | @State(
42 | name = "PsalmValidator",
43 | storages = {@Storage("php.xml")}
44 | )
45 | static class AppPsalmValidatorConfigurationBaseManager extends PsalmValidatorConfigurationBaseManager {
46 | AppPsalmValidatorConfigurationBaseManager() {}
47 | }
48 |
49 | @State(
50 | name = "PsalmValidator",
51 | storages = {@Storage("php.xml")}
52 | )
53 | static class ProjectPsalmValidatorConfigurationBaseManager extends PsalmValidatorConfigurationBaseManager {
54 | ProjectPsalmValidatorConfigurationBaseManager() {}
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/remote/PhpStanValidatorRemoteConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.remote;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.openapi.util.text.StringUtil;
5 | import com.intellij.util.xmlb.annotations.Attribute;
6 | import com.intellij.util.xmlb.annotations.Tag;
7 | import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl;
8 | import com.jetbrains.php.config.interpreters.PhpSdkDependentConfiguration;
9 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfiguration;
10 | import org.jetbrains.annotations.NotNull;
11 | import org.jetbrains.annotations.Nullable;
12 |
13 | /**
14 | * @author Daniel Espendiller
15 | */
16 | @Tag("phpstan_by_interpreter")
17 | public class PhpStanValidatorRemoteConfiguration extends PhpStanValidatorConfiguration implements PhpSdkDependentConfiguration {
18 | private String myInterpreterId;
19 |
20 | @Attribute("interpreter_id")
21 | @Nullable
22 | public String getInterpreterId() {
23 | return this.myInterpreterId;
24 | }
25 |
26 | public void setInterpreterId(@NotNull String interpreterId) {
27 | this.myInterpreterId = interpreterId;
28 | }
29 |
30 | @NotNull
31 | public String getPresentableName(@Nullable Project project) {
32 | return getDefaultName(PhpInterpretersManagerImpl.getInstance(project).findInterpreterName(this.getInterpreterId()));
33 | }
34 |
35 | @NotNull
36 | public String getId() {
37 | String interpreterId = this.getInterpreterId();
38 | return StringUtil.isEmpty(interpreterId) ? "Undefined interpreter" : interpreterId;
39 | }
40 |
41 | @NotNull
42 | public static String getDefaultName(@Nullable String interpreterName) {
43 |
44 | return StringUtil.isEmpty(interpreterName) ? "Undefined interpreter" : "Interpreter: " + interpreterName;
45 | }
46 |
47 | public PhpStanValidatorRemoteConfiguration clone() {
48 | PhpStanValidatorRemoteConfiguration settings = new PhpStanValidatorRemoteConfiguration();
49 | settings.myInterpreterId = this.myInterpreterId;
50 | this.clone(settings);
51 | return settings;
52 | }
53 |
54 | public String serialize(@Nullable String path) {
55 | return path;
56 | }
57 |
58 | public String deserialize(@Nullable String path) {
59 | return path;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/configuration/PhpStanValidatorConfigurationManager.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.configuration;
2 |
3 | import com.intellij.openapi.components.ServiceManager;
4 | import com.intellij.openapi.components.State;
5 | import com.intellij.openapi.components.Storage;
6 | import com.intellij.openapi.project.Project;
7 | import com.intellij.openapi.project.ProjectManager;
8 | import com.jetbrains.php.tools.quality.QualityToolConfigurationManager;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 |
12 | import java.util.List;
13 |
14 | /**
15 | * @author Daniel Espendiller
16 | */
17 | public class PhpStanValidatorConfigurationManager extends QualityToolConfigurationManager {
18 | public PhpStanValidatorConfigurationManager(@Nullable Project project) {
19 | super(project);
20 | if (project != null) {
21 | this.myProjectManager = ServiceManager.getService(project, ProjectPhpStanValidatorConfigurationBaseManager.class);
22 | }
23 |
24 | this.myApplicationManager = ServiceManager.getService(AppPhpStanValidatorConfigurationBaseManager.class);
25 | }
26 |
27 | @NotNull
28 | protected List getDefaultProjectSettings() {
29 | ProjectPhpStanValidatorConfigurationBaseManager service = ServiceManager.getService(
30 | ProjectManager.getInstance().getDefaultProject(),
31 | ProjectPhpStanValidatorConfigurationBaseManager.class
32 | );
33 |
34 | return service.getSettings();
35 | }
36 |
37 | public static PhpStanValidatorConfigurationManager getInstance(@NotNull Project project) {
38 | return ServiceManager.getService(project, PhpStanValidatorConfigurationManager.class);
39 | }
40 |
41 | @State(
42 | name = "PhpStanValidator",
43 | storages = {@Storage("php.xml")}
44 | )
45 | static class AppPhpStanValidatorConfigurationBaseManager extends PhpStanValidatorConfigurationBaseManager {
46 | AppPhpStanValidatorConfigurationBaseManager() {}
47 | }
48 |
49 | @State(
50 | name = "PhpStanValidator",
51 | storages = {@Storage("php.xml")}
52 | )
53 | static class ProjectPhpStanValidatorConfigurationBaseManager extends PhpStanValidatorConfigurationBaseManager {
54 | ProjectPhpStanValidatorConfigurationBaseManager() {}
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/QualityToolUtil.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality;
2 |
3 | import com.intellij.execution.ExecutionException;
4 | import com.intellij.openapi.project.Project;
5 | import com.jetbrains.php.config.interpreters.PhpInterpreter;
6 | import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl;
7 | import com.jetbrains.php.run.remote.PhpRemoteInterpreterManager;
8 | import com.jetbrains.php.tools.quality.QualityToolAnnotatorInfo;
9 | import com.jetbrains.php.util.pathmapper.PhpPathMapper;
10 | import org.jetbrains.annotations.NotNull;
11 | import org.jetbrains.annotations.Nullable;
12 |
13 | /**
14 | * @author Daniel Espendiller
15 | */
16 | public class QualityToolUtil {
17 | /**
18 | * Psalm needs a working directory which is the project; for remote we need to resolve it
19 | *
20 | * We pipe the working directory in general for all our tools
21 | */
22 | @Nullable
23 | private static String getRemotePath(@NotNull Project project, @NotNull String interpreterId) {
24 | PhpInterpreter interpreter = PhpInterpretersManagerImpl.getInstance(project).findInterpreterById(interpreterId);
25 | if (interpreter == null || !interpreter.isRemote()) {
26 | return null;
27 | }
28 |
29 | PhpRemoteInterpreterManager instance = PhpRemoteInterpreterManager.getInstance();
30 | if (instance == null) {
31 | return null;
32 | }
33 |
34 | PhpPathMapper pathMapper;
35 | try {
36 | pathMapper = instance.createPathMapper(project, interpreter.getPhpSdkAdditionalData()).createPathMapper(project);
37 | } catch (ExecutionException e) {
38 | return null;
39 | }
40 |
41 | return pathMapper.getRemoteFilePath(project.getBaseDir());
42 | }
43 |
44 | /**
45 | * Extract the working dir for local or remote resolving
46 | */
47 | @Nullable
48 | public static String getWorkingDirectoryFromAnnotator(@NotNull QualityToolAnnotatorInfo annotatorInfo) {
49 | String interpreterId = annotatorInfo.getInterpreterId();
50 |
51 | String workingDir = null;
52 | if (interpreterId != null) {
53 | workingDir = QualityToolUtil.getRemotePath(annotatorInfo.getProject(), interpreterId);
54 | }
55 |
56 | if (workingDir == null) {
57 | workingDir = annotatorInfo.getProject().getBasePath();
58 | }
59 |
60 | return workingDir;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/form/PsalmValidatorQualityToolConfigurableList.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.form;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.util.ObjectUtils;
5 | import com.jetbrains.php.tools.quality.QualityToolConfigurableForm;
6 | import com.jetbrains.php.tools.quality.QualityToolConfigurableList;
7 | import com.jetbrains.php.tools.quality.QualityToolConfiguration;
8 | import com.jetbrains.php.tools.quality.QualityToolConfigurationProvider;
9 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfiguration;
10 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfigurationManager;
11 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfigurationProvider;
12 | import org.jetbrains.annotations.Nls;
13 | import org.jetbrains.annotations.NotNull;
14 | import org.jetbrains.annotations.Nullable;
15 |
16 | /**
17 | * @author Daniel Espendiller
18 | */
19 | public class PsalmValidatorQualityToolConfigurableList extends QualityToolConfigurableList {
20 | public PsalmValidatorQualityToolConfigurableList(@NotNull Project project, @Nullable String initialElement) {
21 | super(project, PsalmValidatorConfigurationManager.getInstance(project), PsalmValidatorConfiguration::new, PsalmValidatorConfiguration::clone, (settings) -> {
22 | PsalmValidatorConfigurationProvider provider = PsalmValidatorConfigurationProvider.getInstances();
23 | if (provider != null) {
24 | QualityToolConfigurableForm form = provider.createConfigurationForm(project, settings);
25 | if (form != null) {
26 | return form;
27 | }
28 | }
29 |
30 | return new PsalmValidatorConfigurableForm<>(project, settings);
31 | }, initialElement);
32 | this.setSubjectDisplayName("psalm");
33 | }
34 |
35 | @Nullable
36 | protected PsalmValidatorConfiguration getConfiguration(@Nullable QualityToolConfiguration configuration) {
37 | return ObjectUtils.tryCast(configuration, PsalmValidatorConfiguration.class);
38 | }
39 |
40 | @Nullable
41 | @Override
42 | protected QualityToolConfigurationProvider getConfigurationProvider() {
43 | return PsalmValidatorConfigurationProvider.getInstances();
44 | }
45 |
46 | @Nls(capitalization = Nls.Capitalization.Title)
47 | @Override
48 | public String getDisplayName() {
49 | return "Psalm";
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/indexer/dict/TemplateAnnotationUsage.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.indexer.dict;
2 |
3 | import org.apache.commons.lang.builder.HashCodeBuilder;
4 | import org.jetbrains.annotations.NotNull;
5 | import org.jetbrains.annotations.Nullable;
6 |
7 | import java.io.Serializable;
8 | import java.util.Objects;
9 |
10 | /**
11 | * @author Daniel Espendiller
12 | */
13 | public class TemplateAnnotationUsage implements Serializable {
14 | @NotNull
15 | private final String fqn;
16 |
17 | @NotNull
18 | private final Type type;
19 |
20 | @Nullable
21 | private final Integer parameterIndex;
22 |
23 | @Nullable
24 | private String context;
25 |
26 | public enum Type {
27 | FUNCTION_CLASS_STRING,
28 | METHOD_TEMPLATE,
29 | CONSTRUCTOR,
30 | EXTENDS
31 | }
32 |
33 | public TemplateAnnotationUsage(@NotNull String fqn, @NotNull Type type, @Nullable Integer parameterIndex) {
34 | this.fqn = fqn;
35 | this.type = type;
36 | this.parameterIndex = parameterIndex;
37 | }
38 |
39 | public TemplateAnnotationUsage(@NotNull String fqn, @NotNull Type type, @Nullable Integer parameterIndex, @NotNull String context) {
40 | this.fqn = fqn;
41 | this.type = type;
42 | this.parameterIndex = parameterIndex;
43 | this.context = context;
44 | }
45 |
46 | @NotNull
47 | public String getFqn() {
48 | return fqn;
49 | }
50 |
51 | @NotNull
52 | public Type getType() {
53 | return type;
54 | }
55 |
56 | @Nullable
57 | public Integer getParameterIndex() {
58 | return parameterIndex;
59 | }
60 |
61 | @Nullable
62 | public String getContext() {
63 | return context;
64 | }
65 |
66 | @Override
67 | public int hashCode() {
68 | return new HashCodeBuilder()
69 | .append(this.fqn)
70 | .append(this.type.toString())
71 | .append(this.parameterIndex)
72 | .append(this.context)
73 | .toHashCode();
74 | }
75 |
76 | @Override
77 | public boolean equals(Object obj) {
78 | return obj instanceof TemplateAnnotationUsage
79 | && Objects.equals(((TemplateAnnotationUsage) obj).getFqn(), this.fqn)
80 | && Objects.equals(((TemplateAnnotationUsage) obj).getType(), this.type)
81 | && Objects.equals(((TemplateAnnotationUsage) obj).getContext(), this.context)
82 | && Objects.equals(((TemplateAnnotationUsage) obj).getParameterIndex(), this.parameterIndex);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/form/PhpStanValidatorQualityToolConfigurableList.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.form;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.util.ObjectUtils;
5 | import com.jetbrains.php.tools.quality.QualityToolConfigurableForm;
6 | import com.jetbrains.php.tools.quality.QualityToolConfigurableList;
7 | import com.jetbrains.php.tools.quality.QualityToolConfiguration;
8 | import com.jetbrains.php.tools.quality.QualityToolConfigurationProvider;
9 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfiguration;
10 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfigurationManager;
11 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfigurationProvider;
12 | import org.jetbrains.annotations.Nls;
13 | import org.jetbrains.annotations.NotNull;
14 | import org.jetbrains.annotations.Nullable;
15 |
16 | /**
17 | * @author Daniel Espendiller
18 | */
19 | public class PhpStanValidatorQualityToolConfigurableList extends QualityToolConfigurableList {
20 | public PhpStanValidatorQualityToolConfigurableList(@NotNull Project project, @Nullable String initialElement) {
21 | super(project, PhpStanValidatorConfigurationManager.getInstance(project), PhpStanValidatorConfiguration::new, PhpStanValidatorConfiguration::clone, (settings) -> {
22 | PhpStanValidatorConfigurationProvider provider = PhpStanValidatorConfigurationProvider.getInstances();
23 | if (provider != null) {
24 | QualityToolConfigurableForm form = provider.createConfigurationForm(project, settings);
25 | if (form != null) {
26 | return form;
27 | }
28 | }
29 |
30 | return new PhpStanValidatorConfigurableForm<>(project, settings);
31 | }, initialElement);
32 | this.setSubjectDisplayName("phpstan");
33 | }
34 |
35 | @Nullable
36 | protected PhpStanValidatorConfiguration getConfiguration(@Nullable QualityToolConfiguration configuration) {
37 | return ObjectUtils.tryCast(configuration, PhpStanValidatorConfiguration.class);
38 | }
39 |
40 | @Nullable
41 | @Override
42 | protected QualityToolConfigurationProvider getConfigurationProvider() {
43 | return PhpStanValidatorConfigurationProvider.getInstances();
44 | }
45 |
46 | @Nls(capitalization = Nls.Capitalization.Title)
47 | @Override
48 | public String getDisplayName() {
49 | return "PhpStan";
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | # Version names
5 | * 0.x.x: PhpStorm 2020.1+
6 |
7 | ## 0.7.0
8 | * Support full class name resolving based on namespace, alias and use statement for "@template" class definition (Daniel Espendiller)
9 | * Handle IndexOutOfBoundsException when there are no parameters (Simon Podlipsky)
10 | * Force use of Java 11 (Simon Podlipsky)
11 | * Fix impossible split in TemplateAnnotationTypeProvider::complete() (Simon Podlipsky)
12 | * Move Generics inspections group under PHP (Simon Podlipsky)
13 | * Support template extraction also for "as object" (Daniel Espendiller)
14 | * Provide a first working return type resolving for @template and @extends docblocks (Daniel Espendiller)
15 |
16 | ## 0.6.0
17 | * Provide Psalm inspection description (Daniel Espendiller)
18 | * Higher index version for data object usages since last version (Daniel Espendiller)
19 | * Support remote working directory resolving for quality tools (Daniel Espendiller)
20 | * Psalm remote interpreter (Daniel Espendiller)
21 | * Remove phpcs references (dbrekelmans)
22 | * Fix psalm typo (dbrekelmans)
23 | * Add remote interpreter support for PhpStan quality tool (Daniel Espendiller)
24 | * Show full message output on quality tool checking for PHPStan (Daniel Espendiller)
25 | * Fix possible ArrayIndexOutOfBoundsException in type resolving (Daniel Espendiller)
26 | * Update README.md (Andrew Kovalyov)
27 |
28 | ## 0.5.1
29 | * Provide inspection description on how to configure PHPStan (Daniel Espendiller)
30 | * Update since build to drop older PhpStorm versions which are not supported (Daniel Espendiller)
31 | * Fix wrong prefix for inspection (Daniel Espendiller)
32 |
33 | ## 0.5.0
34 | * Add support for Psalm in quality tools inspection (Daniel Espendiller)
35 | * Provide plugin icon (Daniel Espendiller)
36 | * Add support for PHPStan in quality tools inspection (Daniel Espendiller)
37 | * Create description file / Change label for inspections (orklah)
38 | * Add phpstan suffix support for all docblock (Daniel Espendiller)
39 |
40 | ## 0.4.0
41 | * Wrap all doc tag calls into support al suffixes from static frameworks (Daniel Espendiller)
42 | * Provide psalm return type based on class-string return type templates (Daniel Espendiller)
43 |
44 | ## 0.3.1
45 | * Just guessing this will fix Issue [#11](https://github.com/Haehnchen/idea-php-generics-plugin/issues/11) [#10](https://github.com/Haehnchen/idea-php-generics-plugin/issues/10) (HenkPoley)
46 |
47 | ## 0.3.0
48 | * Add inspection for readonly access on property based on @psalm-readonly and @psalm-immutable tag
49 | * Provide a workaround where "@param" lexer is stripping after "array{"
50 |
51 | ## 0.2.0
52 | * Array-Types: Object-like arrays
53 |
54 | ## 0.1.0
55 | * Add psalm string-class inspection for arguments
56 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/inspection/ClassStringLocalInspection.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.inspection;
2 |
3 | import com.intellij.codeInspection.LocalInspectionTool;
4 | import com.intellij.codeInspection.ProblemHighlightType;
5 | import com.intellij.codeInspection.ProblemsHolder;
6 | import com.intellij.psi.PsiElement;
7 | import com.intellij.psi.PsiElementVisitor;
8 | import com.jetbrains.php.lang.psi.elements.*;
9 | import de.espend.idea.php.generics.utils.GenericsUtil;
10 | import org.jetbrains.annotations.NotNull;
11 |
12 | /**
13 | * "@template T as Exception"
14 | * "@param T::class $type"
15 | * "@return T"
16 | */
17 | public class ClassStringLocalInspection extends LocalInspectionTool {
18 | @NotNull
19 | @Override
20 | public PsiElementVisitor buildVisitor(final @NotNull ProblemsHolder holder, boolean isOnTheFly) {
21 | return new PsiElementVisitor() {
22 | @Override
23 | public void visitElement(PsiElement element) {
24 | if(element instanceof StringLiteralExpression || element instanceof ClassConstantReference) {
25 | PsiElement parent = element.getParent();
26 | if (parent instanceof ParameterList) {
27 | invoke(element, holder);
28 | }
29 | }
30 |
31 | super.visitElement(element);
32 | }
33 | };
34 | }
35 |
36 | private void invoke(@NotNull final PsiElement element, @NotNull ProblemsHolder holder) {
37 | String expectedParameterInstanceOf = GenericsUtil.getExpectedParameterInstanceOf(element);
38 | if (expectedParameterInstanceOf == null) {
39 | return;
40 | }
41 |
42 | String content = GenericsUtil.getStringValue(element);
43 |
44 | if (!(element instanceof ClassConstantReference)) {
45 | holder.registerProblem(
46 | element,
47 | String.format("expects class-string<%s>, parent type string(%s) provided", expectedParameterInstanceOf, content != null ? content : "n/a"),
48 | ProblemHighlightType.GENERIC_ERROR_OR_WARNING
49 | );
50 |
51 | return;
52 | }
53 |
54 | if (content == null) {
55 | return;
56 | }
57 |
58 | PhpClass givenClass = GenericsUtil.findClass(element.getProject(), content);
59 | if (givenClass == null) {
60 | return;
61 | }
62 |
63 | PhpClass expectedClass = GenericsUtil.findClass(element.getProject(), expectedParameterInstanceOf);
64 | if (expectedClass == null) {
65 | return;
66 | }
67 |
68 | if (GenericsUtil.isInstanceOf(givenClass, expectedClass)) {
69 | return;
70 | }
71 |
72 | holder.registerProblem(
73 | element,
74 | String.format("expects class-string<%s>, %s::class provided", expectedParameterInstanceOf, content),
75 | ProblemHighlightType.GENERIC_ERROR_OR_WARNING
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem http://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at daniel@espendiller.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/CheckstyleQualityToolMessageProcessor.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality;
2 |
3 | import com.intellij.codeHighlighting.HighlightDisplayLevel;
4 | import com.intellij.codeInsight.intention.IntentionAction;
5 | import com.jetbrains.php.tools.quality.QualityToolAnnotatorInfo;
6 | import com.jetbrains.php.tools.quality.QualityToolMessage;
7 | import com.jetbrains.php.tools.quality.QualityToolXmlMessageProcessor;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.jetbrains.annotations.Nullable;
10 | import org.xml.sax.Attributes;
11 |
12 | /**
13 | * All common tools all output as "checkstyle" format
14 | *
15 | *
16 | *
17 | *
18 | *
19 | *
20 | *
21 | *
22 | *
23 | * @author Daniel Espendiller
24 | */
25 | abstract public class CheckstyleQualityToolMessageProcessor extends QualityToolXmlMessageProcessor {
26 | private final HighlightDisplayLevel myWarningsHighlightLevel;
27 |
28 | public CheckstyleQualityToolMessageProcessor(QualityToolAnnotatorInfo info) {
29 | super(info);
30 | // allow config?
31 | // this.myWarningsHighlightLevel = ((PhpCSValidationInspection)info.getInspection()).getWarningLevel();
32 | this.myWarningsHighlightLevel = HighlightDisplayLevel.WARNING;
33 | }
34 |
35 | protected XMLMessageHandler getXmlMessageHandler() {
36 | return new CheckstyleXmlMessageHandler();
37 | }
38 |
39 | public int getMessageStart(@NotNull String line) {
40 | int messageStart = line.indexOf("");
50 | }
51 |
52 | @NotNull
53 | protected IntentionAction[] getQuickFix(XMLMessageHandler messageHandler) {
54 | return IntentionAction.EMPTY_ARRAY;
55 | }
56 |
57 | @Nullable
58 | protected String getMessagePrefix() {
59 | return "phpstan";
60 | }
61 |
62 | @Nullable
63 | protected HighlightDisplayLevel severityToDisplayLevel(@NotNull QualityToolMessage.Severity severity) {
64 | return QualityToolMessage.Severity.WARNING.equals(severity) ? this.myWarningsHighlightLevel : null;
65 | }
66 |
67 | @NotNull
68 | protected String getQuickFixFamilyName() {
69 | return "PHPStan";
70 | }
71 |
72 | public boolean processStdErrMessages() {
73 | return false;
74 | }
75 |
76 | /**
77 | * Convert to extract the attributes
78 | *
79 | *
80 | */
81 | private static class CheckstyleXmlMessageHandler extends XMLMessageHandler {
82 | private String message;
83 |
84 | protected void parseTag(@NotNull String tagName, @NotNull Attributes attributes) {
85 | if ("error".equals(tagName)) {
86 | this.mySeverity = QualityToolMessage.Severity.ERROR;
87 | } else if ("warning".equals(tagName)) {
88 | this.mySeverity = QualityToolMessage.Severity.WARNING;
89 | }
90 |
91 | this.myLineNumber = parseLineNumber(attributes.getValue("line"));
92 | this.message = attributes.getValue("message");
93 | }
94 |
95 | public String getMessageText() {
96 | return this.message;
97 | }
98 |
99 | public boolean isStatusValid() {
100 | return this.myLineNumber > -1;
101 | }
102 | }
103 | }
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/PhpStanAnnotatorQualityToolAnnotator.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan;
2 |
3 | import com.intellij.codeInspection.LocalInspectionTool;
4 | import com.intellij.execution.ExecutionException;
5 | import com.intellij.openapi.options.Configurable;
6 | import com.intellij.openapi.project.Project;
7 | import com.intellij.openapi.util.text.StringUtil;
8 | import com.intellij.util.PathUtil;
9 | import com.jetbrains.php.config.interpreters.PhpSdkFileTransfer;
10 | import com.jetbrains.php.tools.quality.*;
11 | import com.jetbrains.php.tools.quality.phpCSFixer.PhpCSFixerValidationInspection;
12 | import de.espend.idea.php.quality.CheckstyleQualityToolMessageProcessor;
13 | import de.espend.idea.php.quality.QualityToolUtil;
14 | import de.espend.idea.php.quality.phpstan.blacklist.PhpStanValidatorBlackList;
15 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorProjectConfiguration;
16 | import de.espend.idea.php.quality.phpstan.form.PhpStanValidatorConfigurable;
17 | import org.jetbrains.annotations.NotNull;
18 | import org.jetbrains.annotations.Nullable;
19 |
20 | import java.util.ArrayList;
21 | import java.util.List;
22 |
23 | /**
24 | * @author Daniel Espendiller
25 | */
26 | public class PhpStanAnnotatorQualityToolAnnotator extends QualityToolAnnotator {
27 | public static final PhpStanAnnotatorQualityToolAnnotator INSTANCE = new PhpStanAnnotatorQualityToolAnnotator();
28 |
29 | @NotNull
30 | @Override
31 | protected String getTemporaryFilesFolder() {
32 | return "phpstan_temp.tmp";
33 | }
34 |
35 | @NotNull
36 | @Override
37 | protected String getInspectionId() {
38 | return (new PhpStanFixerValidationInspection()).getID();
39 | }
40 |
41 | @Override
42 | protected QualityToolMessageProcessor createMessageProcessor(@NotNull QualityToolAnnotatorInfo qualityToolAnnotatorInfo) {
43 | return new CheckstyleQualityToolMessageProcessor(qualityToolAnnotatorInfo) {
44 | @Override
45 | protected Configurable getToolConfigurable(@NotNull Project project) {
46 | return new PhpStanValidatorConfigurable(project);
47 | }
48 | };
49 | }
50 |
51 | protected void runTool(@NotNull QualityToolMessageProcessor messageProcessor, @NotNull QualityToolAnnotatorInfo annotatorInfo, @NotNull PhpSdkFileTransfer transfer) throws ExecutionException {
52 | List params = getCommandLineOptions(annotatorInfo.getFilePath());
53 | PhpStanValidatorBlackList blackList = PhpStanValidatorBlackList.getInstance(annotatorInfo.getProject());
54 |
55 | String workingDir = QualityToolUtil.getWorkingDirectoryFromAnnotator(annotatorInfo);
56 | QualityToolProcessCreator.runToolProcess(annotatorInfo, blackList, messageProcessor, workingDir, transfer, params);
57 | if (messageProcessor.getInternalErrorMessage() != null) {
58 | if (annotatorInfo.isOnTheFly()) {
59 | String message = messageProcessor.getInternalErrorMessage().getMessageText();
60 | showProcessErrorMessage(annotatorInfo, blackList, message);
61 | }
62 |
63 | messageProcessor.setFatalError();
64 | }
65 | }
66 |
67 | private List getCommandLineOptions(String filePath) {
68 | ArrayList options = new ArrayList<>();
69 |
70 | options.add("analyse");
71 | options.add("--error-format=checkstyle");
72 | options.add(filePath);
73 |
74 | return options;
75 | }
76 |
77 | @Nullable
78 | protected QualityToolConfiguration getConfiguration(@NotNull Project project, @NotNull LocalInspectionTool inspection) {
79 | try {
80 | return PhpStanValidatorProjectConfiguration.getInstance(project).findSelectedConfiguration(project);
81 | } catch (QualityToolValidationException e) {
82 | return null;
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/PsalmAnnotatorQualityToolAnnotator.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm;
2 |
3 | import com.intellij.codeInspection.LocalInspectionTool;
4 | import com.intellij.execution.ExecutionException;
5 | import com.intellij.openapi.options.Configurable;
6 | import com.intellij.openapi.project.Project;
7 | import com.jetbrains.php.config.interpreters.PhpSdkFileTransfer;
8 | import com.jetbrains.php.tools.quality.*;
9 | import de.espend.idea.php.quality.CheckstyleQualityToolMessageProcessor;
10 | import de.espend.idea.php.quality.QualityToolUtil;
11 | import de.espend.idea.php.quality.psalm.blacklist.PsalmValidatorBlackList;
12 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorProjectConfiguration;
13 | import de.espend.idea.php.quality.psalm.form.PsalmValidatorConfigurable;
14 | import org.jetbrains.annotations.NotNull;
15 | import org.jetbrains.annotations.Nullable;
16 |
17 | import java.util.ArrayList;
18 | import java.util.List;
19 |
20 | /**
21 | * @author Daniel Espendiller
22 | */
23 | public class PsalmAnnotatorQualityToolAnnotator extends QualityToolAnnotator {
24 | public static final PsalmAnnotatorQualityToolAnnotator INSTANCE = new PsalmAnnotatorQualityToolAnnotator();
25 |
26 | @NotNull
27 | @Override
28 | protected String getTemporaryFilesFolder() {
29 | return "psalm_temp.tmp";
30 | }
31 |
32 | @NotNull
33 | @Override
34 | protected String getInspectionId() {
35 | return (new PsalmValidationInspection()).getID();
36 | }
37 |
38 | @Override
39 | protected QualityToolMessageProcessor createMessageProcessor(@NotNull QualityToolAnnotatorInfo qualityToolAnnotatorInfo) {
40 | return new CheckstyleQualityToolMessageProcessor(qualityToolAnnotatorInfo) {
41 | @Override
42 | protected Configurable getToolConfigurable(@NotNull Project project) {
43 | return new PsalmValidatorConfigurable(project);
44 | }
45 |
46 | @Override
47 | protected String getMessagePrefix() {
48 | return "psalm";
49 | }
50 |
51 | @NotNull
52 | @Override
53 | protected String getQuickFixFamilyName() {
54 | return "Psalm";
55 | }
56 | };
57 | }
58 |
59 | protected void runTool(@NotNull QualityToolMessageProcessor messageProcessor, @NotNull QualityToolAnnotatorInfo annotatorInfo, @NotNull PhpSdkFileTransfer transfer) throws ExecutionException {
60 | List params = getCommandLineOptions(annotatorInfo.getFilePath());
61 | PsalmValidatorBlackList blackList = PsalmValidatorBlackList.getInstance(annotatorInfo.getProject());
62 |
63 | String workingDir = QualityToolUtil.getWorkingDirectoryFromAnnotator(annotatorInfo);
64 | QualityToolProcessCreator.runToolProcess(annotatorInfo, blackList, messageProcessor, workingDir, transfer, params);
65 | if (messageProcessor.getInternalErrorMessage() != null) {
66 | if (annotatorInfo.isOnTheFly()) {
67 | String message = messageProcessor.getInternalErrorMessage().getMessageText();
68 | showProcessErrorMessage(annotatorInfo, blackList, message);
69 | }
70 |
71 | messageProcessor.setFatalError();
72 | }
73 | }
74 |
75 | private List getCommandLineOptions(String filePath) {
76 | ArrayList options = new ArrayList<>();
77 |
78 | options.add("--output-format=checkstyle");
79 | options.add(filePath);
80 |
81 | return options;
82 | }
83 |
84 | @Nullable
85 | protected QualityToolConfiguration getConfiguration(@NotNull Project project, @NotNull LocalInspectionTool inspection) {
86 | try {
87 | return PsalmValidatorProjectConfiguration.getInstance(project).findSelectedConfiguration(project);
88 | } catch (QualityToolValidationException e) {
89 | return null;
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/configuration/PsalmValidatorConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.configuration;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.openapi.util.text.StringUtil;
5 | import com.intellij.util.ArrayUtil;
6 | import com.intellij.util.xmlb.annotations.Attribute;
7 | import com.intellij.util.xmlb.annotations.Transient;
8 | import com.jetbrains.php.tools.quality.QualityToolConfiguration;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 |
12 | import java.util.Arrays;
13 | import java.util.stream.Collectors;
14 |
15 | /**
16 | * @author Daniel Espendiller
17 | */
18 | public class PsalmValidatorConfiguration implements QualityToolConfiguration {
19 | private static final String LOCAL = "Local";
20 | private String myPsalmPath = "";
21 | private String myStandards = "";
22 | private int myMaxMessagesPerFile = 50;
23 | private int myTimeoutMs = 5000;
24 |
25 | public PsalmValidatorConfiguration() {
26 | }
27 |
28 | @Transient
29 | public String getToolPath() {
30 | return this.myPsalmPath;
31 | }
32 |
33 | public void setToolPath(String toolPath) {
34 | this.myPsalmPath = toolPath;
35 | }
36 |
37 | @Attribute("tool_path")
38 | @Nullable
39 | public String getSerializedToolPath() {
40 | return this.serialize(this.myPsalmPath);
41 | }
42 |
43 | public void setSerializedToolPath(@Nullable String configurationFilePath) {
44 | this.myPsalmPath = this.deserialize(configurationFilePath);
45 | }
46 |
47 | @Attribute("max_messages_per_file")
48 | public int getMaxMessagesPerFile() {
49 | return this.myMaxMessagesPerFile;
50 | }
51 |
52 | public void setMaxMessagesPerFile(int maxMessagesPerFile) {
53 | this.myMaxMessagesPerFile = maxMessagesPerFile;
54 | }
55 |
56 | @Attribute("standards")
57 | public String getSerializedStandards() {
58 | return this.myStandards;
59 | }
60 |
61 | public void setSerializedStandards(String standards) {
62 | this.myStandards = standards;
63 | }
64 |
65 | @Transient
66 | public String[] getStandards() {
67 | return (String[]) ArrayUtil.append(this.myStandards.split(";"), "Custom");
68 | }
69 |
70 | public void setStandards(String[] standards) {
71 | this.myStandards = (String) Arrays.stream(standards).filter((standard) -> {
72 | return !"Custom".equals(standard);
73 | }).collect(Collectors.joining(";"));
74 | }
75 |
76 | @Attribute("timeout")
77 | public int getTimeout() {
78 | return this.myTimeoutMs;
79 | }
80 |
81 | public void setTimeout(int timeout) {
82 | this.myTimeoutMs = timeout;
83 | }
84 |
85 | @NotNull
86 | public String getPresentableName(@Nullable Project project) {
87 | return this.getId();
88 | }
89 |
90 | @NotNull
91 | public String getId() {
92 | return LOCAL;
93 | }
94 |
95 | @Nullable
96 | public String getInterpreterId() {
97 | return null;
98 | }
99 |
100 | public PsalmValidatorConfiguration clone() {
101 | PsalmValidatorConfiguration settings = new PsalmValidatorConfiguration();
102 | this.clone(settings);
103 | return settings;
104 | }
105 |
106 | public void clone(@NotNull PsalmValidatorConfiguration settings) {
107 | settings.myPsalmPath = this.myPsalmPath;
108 | settings.myStandards = this.myStandards;
109 | settings.myMaxMessagesPerFile = this.myMaxMessagesPerFile;
110 | settings.myTimeoutMs = this.myTimeoutMs;
111 | }
112 |
113 | public int compareTo(@NotNull QualityToolConfiguration o) {
114 | if (!(o instanceof PsalmValidatorConfiguration)) {
115 | return 1;
116 | } else if (StringUtil.equals(this.getPresentableName(null), LOCAL)) {
117 | return -1;
118 | } else {
119 | return StringUtil.equals(o.getPresentableName(null), LOCAL) ? 1 : StringUtil.compare(this.getPresentableName(null), o.getPresentableName(null), false);
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/inspection/PsalmLocalImmutableInspection.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.inspection;
2 |
3 | import com.intellij.codeInspection.LocalInspectionTool;
4 | import com.intellij.codeInspection.ProblemHighlightType;
5 | import com.intellij.codeInspection.ProblemsHolder;
6 | import com.intellij.psi.PsiElement;
7 | import com.intellij.psi.PsiElementVisitor;
8 | import com.intellij.psi.util.PsiTreeUtil;
9 | import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
10 | import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
11 | import com.jetbrains.php.lang.psi.elements.*;
12 | import de.espend.idea.php.generics.utils.GenericsUtil;
13 | import org.jetbrains.annotations.NotNull;
14 |
15 | /**
16 | * Check property assign "$this->a = 'test'" of a readonly property
17 | *
18 | * Supported tags:
19 | * - "@psalm-readonly"
20 | * - "@psalm-immutable"
21 | *
22 | * @author Daniel Espendiller
23 | */
24 | public class PsalmLocalImmutableInspection extends LocalInspectionTool {
25 | @NotNull
26 | @Override
27 | public PsiElementVisitor buildVisitor(final @NotNull ProblemsHolder holder, boolean isOnTheFly) {
28 | return new PsiElementVisitor() {
29 | @Override
30 | public void visitElement(@NotNull PsiElement element) {
31 | // $this->a = 'foobar';
32 | if (element instanceof FieldReference && element.getParent() instanceof AssignmentExpression) {
33 | Function function = PsiTreeUtil.getParentOfType(element, Function.class);
34 |
35 | // not invalid in constructors
36 | if (this.isInvalidWriteScope((FieldReference) element)) {
37 | PsiElement resolve = ((FieldReference) element).resolve();
38 |
39 | // find fields reference with a psalm tag
40 | boolean isReadOnly = false;
41 |
42 | if (resolve instanceof Field) {
43 | // search for "@psalm-readonly" on property level
44 | PhpDocComment docComment = ((Field) resolve).getDocComment();
45 | if (docComment != null) {
46 | isReadOnly = GenericsUtil.getTagElementsByNameForAllFrameworks(docComment, "psalm-readonly").length > 0;
47 | }
48 |
49 | // search for "@psalm-immutable" on class level if not already given on property level
50 | if (!isReadOnly) {
51 | PhpClass containingClass = ((Field) resolve).getContainingClass();
52 | if (containingClass != null) {
53 | PhpDocComment phpDocComment = containingClass.getDocComment();
54 | if (phpDocComment != null) {
55 | isReadOnly = GenericsUtil.getTagElementsByNameForAllFrameworks(phpDocComment, "immutable").length > 0;
56 | }
57 | }
58 | }
59 |
60 | if (isReadOnly) {
61 | holder.registerProblem(
62 | element,
63 | "[psalm] property marked as readonly",
64 | ProblemHighlightType.GENERIC_ERROR
65 | );
66 | }
67 | }
68 | }
69 | }
70 |
71 | super.visitElement(element);
72 | }
73 |
74 | /**
75 | * Check if value write is disallowed to property eg. inside a class constructor
76 | */
77 | private boolean isInvalidWriteScope(@NotNull FieldReference fieldReference) {
78 | // @TODO: this should maybe optimized eg hint: anonymous classes
79 | Function function = PsiTreeUtil.getParentOfType(fieldReference, Function.class);
80 | return function == null || !"__construct".equalsIgnoreCase(function.getName());
81 | }
82 | };
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/configuration/PhpStanValidatorConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.configuration;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.openapi.util.text.StringUtil;
5 | import com.intellij.util.ArrayUtil;
6 | import com.intellij.util.xmlb.annotations.Attribute;
7 | import com.intellij.util.xmlb.annotations.Transient;
8 | import com.jetbrains.php.tools.quality.QualityToolConfiguration;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 |
12 | import java.util.Arrays;
13 | import java.util.stream.Collectors;
14 |
15 | /**
16 | * @author Daniel Espendiller
17 | */
18 | public class PhpStanValidatorConfiguration implements QualityToolConfiguration {
19 | private static final String LOCAL = "Local";
20 | private String myPHPStanPath = "";
21 | private String myStandards = "";
22 | private int myMaxMessagesPerFile = 50;
23 | private int myTimeoutMs = 5000;
24 |
25 | public PhpStanValidatorConfiguration() {
26 | }
27 |
28 | @Transient
29 | public String getToolPath() {
30 | return this.myPHPStanPath;
31 | }
32 |
33 | public void setToolPath(String toolPath) {
34 | this.myPHPStanPath = toolPath;
35 | }
36 |
37 | @Attribute("tool_path")
38 | @Nullable
39 | public String getSerializedToolPath() {
40 | return this.serialize(this.myPHPStanPath);
41 | }
42 |
43 | public void setSerializedToolPath(@Nullable String configurationFilePath) {
44 | this.myPHPStanPath = this.deserialize(configurationFilePath);
45 | }
46 |
47 | @Attribute("max_messages_per_file")
48 | public int getMaxMessagesPerFile() {
49 | return this.myMaxMessagesPerFile;
50 | }
51 |
52 | public void setMaxMessagesPerFile(int maxMessagesPerFile) {
53 | this.myMaxMessagesPerFile = maxMessagesPerFile;
54 | }
55 |
56 | @Attribute("standards")
57 | public String getSerializedStandards() {
58 | return this.myStandards;
59 | }
60 |
61 | public void setSerializedStandards(String standards) {
62 | this.myStandards = standards;
63 | }
64 |
65 | @Transient
66 | public String[] getStandards() {
67 | return (String[]) ArrayUtil.append(this.myStandards.split(";"), "Custom");
68 | }
69 |
70 | public void setStandards(String[] standards) {
71 | this.myStandards = (String) Arrays.stream(standards).filter((standard) -> {
72 | return !"Custom".equals(standard);
73 | }).collect(Collectors.joining(";"));
74 | }
75 |
76 | @Attribute("timeout")
77 | public int getTimeout() {
78 | return this.myTimeoutMs;
79 | }
80 |
81 | public void setTimeout(int timeout) {
82 | this.myTimeoutMs = timeout;
83 | }
84 |
85 | @NotNull
86 | public String getPresentableName(@Nullable Project project) {
87 | return this.getId();
88 | }
89 |
90 | @NotNull
91 | public String getId() {
92 | return "Local";
93 | }
94 |
95 | @Nullable
96 | public String getInterpreterId() {
97 | return null;
98 | }
99 |
100 | public PhpStanValidatorConfiguration clone() {
101 | PhpStanValidatorConfiguration settings = new PhpStanValidatorConfiguration();
102 | this.clone(settings);
103 | return settings;
104 | }
105 |
106 | public void clone(@NotNull PhpStanValidatorConfiguration settings) {
107 | settings.myPHPStanPath = this.myPHPStanPath;
108 | settings.myStandards = this.myStandards;
109 | settings.myMaxMessagesPerFile = this.myMaxMessagesPerFile;
110 | settings.myTimeoutMs = this.myTimeoutMs;
111 | }
112 |
113 | public int compareTo(@NotNull QualityToolConfiguration o) {
114 | if (!(o instanceof PhpStanValidatorConfiguration)) {
115 | return 1;
116 | } else if (StringUtil.equals(this.getPresentableName(null), "Local")) {
117 | return -1;
118 | } else {
119 | return StringUtil.equals(o.getPresentableName(null), "Local") ? 1 : StringUtil.compare(this.getPresentableName(null), o.getPresentableName(null), false);
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/psalm/remote/PsalmValidatorRemoteConfigurationProvider.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.psalm.remote;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.openapi.util.text.StringUtil;
5 | import com.intellij.util.PathMappingSettings;
6 | import com.intellij.util.xmlb.XmlSerializer;
7 | import com.jetbrains.php.config.interpreters.PhpInterpreter;
8 | import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl;
9 | import com.jetbrains.php.config.interpreters.PhpSdkAdditionalData;
10 | import com.jetbrains.php.remote.tools.quality.QualityToolByInterpreterConfigurableForm;
11 | import com.jetbrains.php.run.remote.PhpRemoteInterpreterManager;
12 | import com.jetbrains.php.tools.quality.QualityToolConfigurableForm;
13 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfiguration;
14 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfigurationManager;
15 | import de.espend.idea.php.quality.psalm.configuration.PsalmValidatorConfigurationProvider;
16 | import de.espend.idea.php.quality.psalm.form.PsalmValidatorConfigurableForm;
17 | import org.jdom.Element;
18 | import org.jetbrains.annotations.NotNull;
19 | import org.jetbrains.annotations.Nullable;
20 |
21 | import java.util.List;
22 |
23 | /**
24 | * @author Daniel Espendiller
25 | */
26 | public class PsalmValidatorRemoteConfigurationProvider extends PsalmValidatorConfigurationProvider {
27 | public String getConfigurationName(@Nullable String interpreterName) {
28 | return PsalmValidatorRemoteConfiguration.getDefaultName(interpreterName);
29 | }
30 |
31 | public boolean canLoad(@NotNull String tagName) {
32 | return StringUtil.equals(tagName, "psalm_by_interpreter");
33 | }
34 |
35 | @Nullable
36 | public PsalmValidatorConfiguration load(@NotNull Element element) {
37 | return XmlSerializer.deserialize(element, PsalmValidatorRemoteConfiguration.class);
38 | }
39 |
40 | @Nullable
41 | public QualityToolConfigurableForm createConfigurationForm(@NotNull Project project, @NotNull PsalmValidatorConfiguration settings) {
42 | if (settings instanceof PsalmValidatorRemoteConfiguration) {
43 | PsalmValidatorRemoteConfiguration remoteConfiguration = (PsalmValidatorRemoteConfiguration)settings;
44 | PsalmValidatorConfigurableForm delegate = new PsalmValidatorConfigurableForm<>(project, remoteConfiguration);
45 | return new QualityToolByInterpreterConfigurableForm<>(project, remoteConfiguration, delegate);
46 | } else {
47 | return null;
48 | }
49 | }
50 |
51 | public PsalmValidatorConfiguration createNewInstance(@Nullable Project project, @NotNull List existingSettings) {
52 | PsalmValidatorInterpreterDialog dialog = new PsalmValidatorInterpreterDialog(project, existingSettings);
53 | if (dialog.showAndGet()) {
54 | String id = PhpInterpretersManagerImpl.getInstance(project).findInterpreterId(dialog.getSelectedInterpreterName());
55 | if (StringUtil.isNotEmpty(id)) {
56 | PsalmValidatorRemoteConfiguration settings = new PsalmValidatorRemoteConfiguration();
57 | settings.setInterpreterId(id);
58 | PhpSdkAdditionalData data = PhpInterpretersManagerImpl.getInstance(project).findInterpreterDataById(id);
59 | PhpRemoteInterpreterManager manager = PhpRemoteInterpreterManager.getInstance();
60 | if (manager != null && data != null) {
61 | PathMappingSettings mappings = manager.createPathMappings(project, data);
62 | if (project != null) {
63 | this.fillSettingsByDefaultValue(settings, PsalmValidatorConfigurationManager.getInstance(project).getLocalSettings(), (localPath) -> localPath == null ? null : mappings.convertToRemote(localPath));
64 | }
65 | }
66 |
67 | return settings;
68 | }
69 | }
70 |
71 | return null;
72 | }
73 |
74 | public PsalmValidatorConfiguration createConfigurationByInterpreter(@NotNull PhpInterpreter interpreter) {
75 | PsalmValidatorRemoteConfiguration settings = new PsalmValidatorRemoteConfiguration();
76 | settings.setInterpreterId(interpreter.getId());
77 | return settings;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/quality/phpstan/remote/PhpStanValidatorRemoteConfigurationProvider.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.quality.phpstan.remote;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.openapi.util.text.StringUtil;
5 | import com.intellij.util.PathMappingSettings;
6 | import com.intellij.util.xmlb.XmlSerializer;
7 | import com.jetbrains.php.config.interpreters.PhpInterpreter;
8 | import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl;
9 | import com.jetbrains.php.config.interpreters.PhpSdkAdditionalData;
10 | import com.jetbrains.php.remote.tools.quality.QualityToolByInterpreterConfigurableForm;
11 | import com.jetbrains.php.run.remote.PhpRemoteInterpreterManager;
12 | import com.jetbrains.php.tools.quality.QualityToolConfigurableForm;
13 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfiguration;
14 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfigurationManager;
15 | import de.espend.idea.php.quality.phpstan.configuration.PhpStanValidatorConfigurationProvider;
16 | import de.espend.idea.php.quality.phpstan.form.PhpStanValidatorConfigurableForm;
17 | import org.jdom.Element;
18 | import org.jetbrains.annotations.NotNull;
19 | import org.jetbrains.annotations.Nullable;
20 |
21 | import java.util.List;
22 |
23 | /**
24 | * @author Daniel Espendiller
25 | */
26 | public class PhpStanValidatorRemoteConfigurationProvider extends PhpStanValidatorConfigurationProvider {
27 | public String getConfigurationName(@Nullable String interpreterName) {
28 | return PhpStanValidatorRemoteConfiguration.getDefaultName(interpreterName);
29 | }
30 |
31 | public boolean canLoad(@NotNull String tagName) {
32 | return StringUtil.equals(tagName, "phpstan_by_interpreter");
33 | }
34 |
35 | @Nullable
36 | public PhpStanValidatorConfiguration load(@NotNull Element element) {
37 | return XmlSerializer.deserialize(element, PhpStanValidatorRemoteConfiguration.class);
38 | }
39 |
40 | @Nullable
41 | public QualityToolConfigurableForm createConfigurationForm(@NotNull Project project, @NotNull PhpStanValidatorConfiguration settings) {
42 | if (settings instanceof PhpStanValidatorRemoteConfiguration) {
43 | PhpStanValidatorRemoteConfiguration remoteConfiguration = (PhpStanValidatorRemoteConfiguration)settings;
44 | PhpStanValidatorConfigurableForm delegate = new PhpStanValidatorConfigurableForm<>(project, remoteConfiguration);
45 | return new QualityToolByInterpreterConfigurableForm<>(project, remoteConfiguration, delegate);
46 | } else {
47 | return null;
48 | }
49 | }
50 |
51 | public PhpStanValidatorConfiguration createNewInstance(@Nullable Project project, @NotNull List existingSettings) {
52 | PhpStanValidatorInterpreterDialog dialog = new PhpStanValidatorInterpreterDialog(project, existingSettings);
53 | if (dialog.showAndGet()) {
54 | String id = PhpInterpretersManagerImpl.getInstance(project).findInterpreterId(dialog.getSelectedInterpreterName());
55 | if (StringUtil.isNotEmpty(id)) {
56 | PhpStanValidatorRemoteConfiguration settings = new PhpStanValidatorRemoteConfiguration();
57 | settings.setInterpreterId(id);
58 | PhpSdkAdditionalData data = PhpInterpretersManagerImpl.getInstance(project).findInterpreterDataById(id);
59 | PhpRemoteInterpreterManager manager = PhpRemoteInterpreterManager.getInstance();
60 | if (manager != null && data != null) {
61 | PathMappingSettings mappings = manager.createPathMappings(project, data);
62 | if (project != null) {
63 | this.fillSettingsByDefaultValue(settings, PhpStanValidatorConfigurationManager.getInstance(project).getLocalSettings(), (localPath) -> localPath == null ? null : mappings.convertToRemote(localPath));
64 | }
65 | }
66 |
67 | return settings;
68 | }
69 | }
70 |
71 | return null;
72 | }
73 |
74 | public PhpStanValidatorConfiguration createConfigurationByInterpreter(@NotNull PhpInterpreter interpreter) {
75 | PhpStanValidatorRemoteConfiguration settings = new PhpStanValidatorRemoteConfiguration();
76 | settings.setInterpreterId(interpreter.getId());
77 | return settings;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/test/java/de/espend/idea/php/generics/tests/utils/GenericsUtilTest.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.tests.utils;
2 |
3 | import com.intellij.psi.PsiElement;
4 | import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
5 | import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
6 | import com.jetbrains.php.lang.psi.PhpPsiElementFactory;
7 | import com.jetbrains.php.lang.psi.elements.Function;
8 | import com.jetbrains.php.lang.psi.elements.MethodReference;
9 | import de.espend.idea.php.generics.dict.ParameterArrayType;
10 | import de.espend.idea.php.generics.tests.AnnotationLightCodeInsightFixtureTestCase;
11 | import de.espend.idea.php.generics.utils.GenericsUtil;
12 |
13 | import java.util.*;
14 |
15 | public class GenericsUtilTest extends AnnotationLightCodeInsightFixtureTestCase {
16 | public void setUp() throws Exception {
17 | super.setUp();
18 | myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("fixtures.php"));
19 | }
20 |
21 | public String getTestDataPath() {
22 | return "src/test/java/de/espend/idea/php/generics/tests/utils/fixtures";
23 | }
24 |
25 | public void testThatTypeTemplateIsExtracted() {
26 | MethodReference methodReference = PhpPsiElementFactory.createMethodReference(getProject(), "C::a('foo', 'foobar')");
27 |
28 | PsiElement[] parameters = methodReference.getParameterList().getParameters();
29 |
30 | assertEquals("Exception", GenericsUtil.getExpectedParameterInstanceOf(parameters[0]));
31 | assertNull(GenericsUtil.getExpectedParameterInstanceOf(parameters[1]));
32 | }
33 |
34 | public void testThatTypeTemplateIsExtractedForImports() {
35 | MethodReference methodReference = PhpPsiElementFactory.createMethodReference(getProject(), "C::b('foo', 'foobar')");
36 |
37 | PsiElement[] parameters = methodReference.getParameterList().getParameters();
38 |
39 | assertEquals("Foobar\\Foo", GenericsUtil.getExpectedParameterInstanceOf(parameters[0]));
40 | assertEquals("Foobar\\Foo", GenericsUtil.getExpectedParameterInstanceOf(parameters[1]));
41 | }
42 |
43 | public void testThatParamArrayElementsAreExtracted() {
44 | PhpDocComment phpDocComment = PhpPsiElementFactory.createPhpPsiFromText(getProject(), PhpDocComment.class, "" +
45 | "/**\n" +
46 | "* @psalm-param array{foo: Foo, ?bar: int | string} $foobar\n" +
47 | "*/\n" +
48 | "function test($foobar) {}\n"
49 | );
50 |
51 | PhpDocTag[] tagElementsByName = phpDocComment.getTagElementsByName("@psalm-param");
52 | String tagValue = tagElementsByName[0].getTagValue();
53 |
54 | Collection vars = GenericsUtil.getParameterArrayTypes(tagValue, "foobar", tagElementsByName[0]);
55 |
56 | ParameterArrayType foo = Objects.requireNonNull(vars).stream().filter(parameterArrayType -> parameterArrayType.getKey().equalsIgnoreCase("foo")).findFirst().get();
57 | assertFalse(foo.isOptional());
58 | assertContainsElements(Collections.singletonList("Foo"), foo.getValues());
59 |
60 | ParameterArrayType foobar = Objects.requireNonNull(vars).stream().filter(parameterArrayType -> parameterArrayType.getKey().equalsIgnoreCase("bar")).findFirst().get();
61 | assertTrue(foobar.isOptional());
62 | assertContainsElements(Arrays.asList("string", "int"), foobar.getValues());
63 | }
64 |
65 | public void testThatReturnElementsAreExtracted() {
66 | Function function = PhpPsiElementFactory.createPhpPsiFromText(getProject(), Function.class, "" +
67 | "/**\n" +
68 | "* @psalm-return array{foo: Foo, ?bar: int | string}\n" +
69 | "* @return array{foo2: Foo, ?bar2: int | string}\n" +
70 | "*/" +
71 | "function test() {}\n"
72 | );
73 |
74 | Collection vars = GenericsUtil.getReturnArrayTypes(function);
75 |
76 | ParameterArrayType bar = Objects.requireNonNull(vars).stream().filter(parameterArrayType -> parameterArrayType.getKey().equalsIgnoreCase("bar")).findFirst().get();
77 | assertTrue(bar.isOptional());
78 | assertContainsElements(Arrays.asList("string", "int"), bar.getValues());
79 |
80 | ParameterArrayType bar2 = Objects.requireNonNull(vars).stream().filter(parameterArrayType -> parameterArrayType.getKey().equalsIgnoreCase("bar2")).findFirst().get();
81 | assertTrue(bar2.isOptional());
82 | assertContainsElements(Arrays.asList("string", "int"), bar2.getValues());
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/CompletionNavigationProvider.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics;
2 |
3 | import com.intellij.codeInsight.completion.*;
4 | import com.intellij.codeInsight.lookup.LookupElement;
5 | import com.intellij.codeInsight.lookup.LookupElementBuilder;
6 | import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler;
7 | import com.intellij.openapi.editor.Editor;
8 | import com.intellij.psi.PsiElement;
9 | import com.intellij.psi.util.PsiTreeUtil;
10 | import com.intellij.util.ProcessingContext;
11 | import com.jetbrains.php.PhpIcons;
12 | import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression;
13 | import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
14 | import de.espend.idea.php.generics.dict.ParameterArrayType;
15 | import de.espend.idea.php.generics.utils.GenericsUtil;
16 | import de.espend.idea.php.generics.utils.PhpElementsUtil;
17 | import org.apache.commons.lang.StringUtils;
18 | import org.jetbrains.annotations.NotNull;
19 | import org.jetbrains.annotations.Nullable;
20 |
21 | import java.util.Collection;
22 | import java.util.HashSet;
23 | import java.util.stream.Collectors;
24 |
25 | /**
26 | * @author Daniel Espendiller
27 | */
28 | public class CompletionNavigationProvider {
29 | public static class Completion extends CompletionContributor {
30 | public Completion() {
31 | extend(CompletionType.BASIC, PhpElementsUtil.getParameterListArrayValuePattern(), new ArrayParameterCompletionProvider());
32 | }
33 | }
34 |
35 | public static class GotoDeclaration implements GotoDeclarationHandler {
36 | @Nullable
37 | @Override
38 | public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement, int offset, Editor editor) {
39 | if (sourceElement == null) {
40 | return new PsiElement[0];
41 | }
42 |
43 | Collection psiElements = new HashSet<>();
44 |
45 | PsiElement parent = sourceElement.getParent();
46 | if (parent instanceof StringLiteralExpression && PhpElementsUtil.getParameterListArrayValuePattern().accepts(sourceElement)) {
47 | psiElements.addAll(collectArrayKeyParameterTargets(parent));
48 | }
49 |
50 | return psiElements.toArray(new PsiElement[0]);
51 | }
52 |
53 | private Collection collectArrayKeyParameterTargets(PsiElement psiElement) {
54 | Collection psiElements = new HashSet<>();
55 |
56 | ArrayCreationExpression parentOfType = PsiTreeUtil.getParentOfType(psiElement, ArrayCreationExpression.class);
57 |
58 | if (parentOfType != null) {
59 | String contents = ((StringLiteralExpression) psiElement).getContents();
60 |
61 | psiElements.addAll(GenericsUtil.getTypesForParameter(parentOfType).stream()
62 | .filter(parameterArrayType -> parameterArrayType.getKey().equalsIgnoreCase(contents))
63 | .map(ParameterArrayType::getContext).collect(Collectors.toSet())
64 | );
65 | }
66 |
67 | return psiElements;
68 | }
69 | }
70 |
71 | /**
72 | * foo([''])
73 | * foo(['' => 'foobar'])
74 | */
75 | private static class ArrayParameterCompletionProvider extends CompletionProvider {
76 | @Override
77 | protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet result) {
78 | PsiElement position = parameters.getPosition();
79 |
80 | ArrayCreationExpression parentOfType = PsiTreeUtil.getParentOfType(position, ArrayCreationExpression.class);
81 | if (parentOfType != null) {
82 | result.addAllElements(GenericsUtil.getTypesForParameter(parentOfType).stream()
83 | .map(CompletionNavigationProvider::createParameterArrayTypeLookupElement)
84 | .collect(Collectors.toSet())
85 | );
86 | }
87 | }
88 | }
89 |
90 | @NotNull
91 | private static LookupElement createParameterArrayTypeLookupElement(@NotNull ParameterArrayType type) {
92 | LookupElementBuilder lookupElementBuilder = LookupElementBuilder.create(type.getKey())
93 | .withIcon(PhpIcons.FIELD);
94 |
95 | String types = StringUtils.join(type.getValues(), '|');
96 | if (type.isOptional()) {
97 | types = "(optional) " + types;
98 | }
99 |
100 | return lookupElementBuilder.withTypeText(types);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/test/java/de/espend/idea/php/generics/tests/indexer/fixtures/classes.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | public function __construct(array $data = []) {}
14 | /**
15 | * @return T
16 | */
17 | public function get(string $key) {}
18 | /**
19 | * @param T $value
20 | */
21 | public function set(string $key, $value): void {}
22 | }
23 |
24 | /**
25 | * @psalm-template X
26 | */
27 | class PsalmMap
28 | {
29 | /**
30 | * @param array
31 | */
32 | public function __construct(array $data = []) {}
33 | /**
34 | * @return X
35 | */
36 | public function get(string $key) {}
37 | /**
38 | * @param X $value
39 | */
40 | public function set(string $key, $value): void {}
41 | }
42 |
43 | /**
44 | * @template ZzzA
45 | */
46 | class Zzz
47 | {
48 | /**
49 | * @param array
50 | */
51 | public function __construct(array $data = []) {}
52 | /**
53 | * @return ZzzA
54 | */
55 | public function get(string $key) {}
56 | /**
57 | * @param ZzzA $value
58 | */
59 | public function set(string $key, $value): void {}
60 | }
61 |
62 | /**
63 | * @template
64 | */
65 | class Bar
66 | {
67 | /**
68 | * @param array
69 | */
70 | public function __construct(array $data = []) {}
71 | /**
72 | * @return X
73 | */
74 | public function get(string $key) {}
75 | /**
76 | * @param X $value
77 | */
78 | public function set(string $key, $value): void {}
79 | }
80 |
81 | }
82 |
83 | namespace Instantiator\Foobar
84 | {
85 | class Foobar
86 | {
87 | /**
88 | * @template T
89 | * @psalm-param class-string $class
90 | * @return T
91 | */
92 | function _barInstantiator(string $class) {
93 | return new $class();
94 | }
95 | }
96 | }
97 |
98 | namespace Template
99 | {
100 | /**
101 | * @template T
102 | */
103 | class MyTemplateImpl
104 | {
105 | /**
106 | * @psalm-return T
107 | */
108 | public function getValue()
109 | {
110 | }
111 |
112 | /**
113 | * @return T
114 | */
115 | public function getValueReturn()
116 | {
117 | }
118 | }
119 |
120 | /**
121 | * @template T as object
122 | */
123 | class MyTemplateObject
124 | {
125 | /**
126 | * @return T
127 | */
128 | public function getValue()
129 | {
130 | }
131 | }
132 | }
133 |
134 | namespace Extend\Types
135 | {
136 | class Foobar {}
137 | }
138 |
139 | namespace Extended\Classes
140 | {
141 | use App\Foo\Bar\MyContainer;
142 | use Extend\Types\Foobar;
143 |
144 | use Extend\Types as Bar;
145 | use App\Foo\Bar as BarAlias;
146 |
147 | /**
148 | * @extends \App\Foo\Bar\MyContainer<\DateTime>
149 | */
150 | class MyExtendsImpl
151 | {
152 | }
153 |
154 | /**
155 | * @psalm-extends \App\Foo\Bar\MyContainer
156 | */
157 | class MyExtendsImplPsalm
158 | {
159 | }
160 |
161 | /**
162 | * @phpstan-extends \App\Foo\Bar\MyContainer<\DateTime>
163 | */
164 | class MyExtendsImplPhpStan
165 | {
166 | }
167 |
168 | /**
169 | * @extends MyContainer
170 | */
171 | class MyExtendsImplUse extends MyContainer
172 | {
173 | }
174 |
175 | /**
176 | * @extends BarAlias\MyContainer
177 | */
178 | class MyExtendsImplUseAlias extends MyContainer
179 | {
180 | }
181 | }
182 |
183 | namespace
184 | {
185 | /**
186 | * @template T
187 | * @psalm-param class-string $class
188 | * @return T
189 | */
190 | function instantiator(string $class) {
191 | return new $class();
192 | }
193 |
194 | /**
195 | * @template T
196 | * @param class-string $class
197 | * @return T
198 | */
199 | function instantiatorParam(string $class) {
200 | return new $class();
201 | }
202 |
203 | /**
204 | * @template T
205 | * @param class-string $class
206 | * @psalm-return T
207 | */
208 | function instantiatorReturn(string $class) {
209 | return new $class();
210 | }
211 |
212 | /**
213 | * @phpstan-template T
214 | * @phpstan-param class-string $class
215 | * @phpstan-return T
216 | */
217 | function instantiatorPhpStan(string $class) {
218 | return new $class();
219 | }
220 |
221 | /**
222 | * @phpstan-template T as object
223 | * @phpstan-param class-string $class
224 | * @phpstan-return T as object
225 | */
226 | function instantiatorPhpStanAsObject(string $foobar, string $class) {
227 | return new $class();
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IntelliJ IDEA / PhpStorm PHPStan / Psalm / Generics
2 |
3 | **Use Replacements:**
4 |
5 | - https://github.com/JetBrains/phpstorm-psalm-plugin
6 | - https://github.com/JetBrains/phpstorm-phpstan-plugin
7 |
8 |
9 | [](https://travis-ci.org/Haehnchen/idea-php-generics-plugin)
10 | [](https://plugins.jetbrains.com/plugin/12754)
11 | [](https://plugins.jetbrains.com/plugin/12754)
12 | [](https://plugins.jetbrains.com/plugin/12754)
13 | [](https://www.paypal.me/DanielEspendiller)
14 |
15 |
16 | Key | Value
17 | ----------- | -----------
18 | Plugin url | https://plugins.jetbrains.com/plugin/12754-php-generics
19 | Id | de.espend.idea.php.generics
20 | Changelog | [CHANGELOG](CHANGELOG.md)
21 |
22 |
23 | !!! Work in progress !!!
24 |
25 | ## Supported
26 |
27 | ### types
28 |
29 | ```php
30 | /**
31 | * @[psalm-|phpstan-]template T
32 | * @[psalm-|phpstan-]param class-string $class
33 | * @[psalm-|phpstan-]return T
34 | */
35 | function instantiator(string $class) {
36 | return new $class();
37 | }
38 |
39 | class Foo {}
40 |
41 | $a = instantiator(Foo::class); // Psalm knows the result is an object of type Foo
42 | ```
43 |
44 | ### class-string
45 |
46 | * Inspections for not given wrong parameter
47 |
48 | ```php
49 | /**
50 | * @[psalm-|phpstan-]template T as Exception
51 | * @[psalm-|phpstan-]param T::class $type
52 | * @return T
53 | */
54 | function a(string $type): Exception
55 | {
56 | return new $type;
57 | }
58 | ```
59 |
60 | ### templates
61 |
62 | ```php
63 | $collection = new FooCollection();
64 |
65 | // its now a type of "Foobar"
66 | $foobar = $collection->getValue();
67 | $foobar->getFoobar(); // method is clickable and autocompletes
68 | ```
69 |
70 | ```php
71 | /**
72 | * @[psalm-|phpstan-]template T
73 | */
74 | class Collection
75 | {
76 | /**
77 | * @[psalm-|phpstan-]return T
78 | */
79 | public function getValue() {}
80 | }
81 |
82 | /**
83 | * @[psalm-|phpstan-]extends Collection
84 | */
85 | class FooCollection extends Collection {}
86 |
87 | class Foobar
88 | {
89 | public function getFoobar() {}
90 | }
91 | ```
92 |
93 | ### Object-like arrays
94 |
95 | https://psalm.dev/docs/annotating_code/type_syntax/array_types/
96 |
97 | ```php
98 | a(['' => ''])
99 |
100 | ```
101 |
102 | ```php
103 | /**
104 | * @[psalm-|phpstan-]param array{foo: string, bar: int} $type
105 | */
106 | function a(array $type): Exception
107 | {
108 | }
109 | ```
110 |
111 |
112 | ### psalm-immutable and psalm-readonly
113 |
114 | Inspection to show disallowed write access
115 |
116 | ```
117 | class PsalmReadOnly {
118 | /**
119 | * @psalm-readonly
120 | */
121 | public string $readOnly;
122 | }
123 |
124 | /**
125 | * @psalm-immutable
126 | */
127 | class PsalmImmutable {
128 | public string $readOnly;
129 | }
130 | ```
131 |
132 | Follows into errors hints
133 |
134 | ```
135 | (new PsalmReadOnly())->readOnly = 'test';
136 | (new PsalmImmutable())->readOnly = 'test';
137 | ```
138 |
139 | ## Quality Tools
140 |
141 | Provides support for quality tools inspection via directly call PHPStan or Psalm reporting via `codestyle` format
142 |
143 | ## Limitation / Issues
144 |
145 | * Inconsistently PhpStorm docblock parser: https://youtrack.jetbrains.com/issue/WI-47644
146 |
147 | ## Screenshots
148 |
149 | 
150 | 
151 | 
152 | 
153 | 
154 |
155 | ## TODO
156 |
157 | https://youtrack.jetbrains.com/issue/WI-47158
158 |
159 | ```php
160 | /**
161 | * @template T
162 | */
163 | class Map
164 | {
165 | /**
166 | * @param array
167 | */
168 | public function __construct(array $data = []) {}
169 | /**
170 | * @return T
171 | */
172 | public function get(string $key) {}
173 | /**
174 | * @param T $value
175 | */
176 | public function set(string $key, $value): void {}
177 | }
178 | // Automatically inferred as Map
179 | $map = new Map([0 => 'Foo', 1 => 'Bar']);
180 | $map->set(2, true); // Expected string
181 | ```
182 |
183 |
184 | https://youtrack.jetbrains.com/issue/WI-45248
185 |
186 |
187 | ```php
188 | class Assert
189 | {
190 | /**
191 | * @psalm-template ExpectedType of object
192 | * @psalm-param class-string $class
193 | * @psalm-assert ExpectedType $value
194 | */
195 | public static function isInstanceOf($value, $class, $message = '')
196 | {
197 | }
198 | }
199 | ```
200 |
201 |
202 | https://github.com/phpstan/phpdoc-parser/pull/30
203 |
204 | ```php
205 |
206 | /**
207 | * @param array{'foo': int, "bar": string} $a
208 | * @param array{0: int, 1?: int} $a
209 | * @param array{int, int} $a
210 | * @param array{foo: int, bar: string} $a
211 | * @param array{foo:string, bar:?int} $a
212 | */
213 | ```
214 |
215 | Others
216 |
217 | ```php
218 | /** @var array */
219 | ```
220 |
--------------------------------------------------------------------------------
/src/test/java/de/espend/idea/php/generics/tests/indexer/TemplateAnnotationIndexTest.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.tests.indexer;
2 |
3 | import de.espend.idea.php.generics.indexer.TemplateAnnotationIndex;
4 | import de.espend.idea.php.generics.indexer.dict.TemplateAnnotationUsage;
5 | import de.espend.idea.php.generics.tests.AnnotationLightCodeInsightFixtureTestCase;
6 |
7 | /**
8 | * @author Daniel Espendiller
9 | * @see TemplateAnnotationIndex
10 | */
11 | public class TemplateAnnotationIndexTest extends AnnotationLightCodeInsightFixtureTestCase {
12 | public void setUp() throws Exception {
13 | super.setUp();
14 | myFixture.copyFileToProject("classes.php");
15 | }
16 |
17 | public String getTestDataPath() {
18 | return "src/test/java/de/espend/idea/php/generics/tests/indexer/fixtures";
19 | }
20 |
21 | public void testThatTemplateClassIsInIndex() {
22 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Foo\\Map.get");
23 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Foo\\PsalmMap.get");
24 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Foo\\Zzz.get");
25 | assertIndexNotContains(TemplateAnnotationIndex.KEY, "\\Foo\\Bar");
26 |
27 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Instantiator\\Foobar\\Foobar._barInstantiator");
28 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\instantiator");
29 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\instantiatorParam");
30 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\instantiatorReturn");
31 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\instantiatorPhpStan");
32 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\instantiatorPhpStanAsObject");
33 |
34 | assertIndexContainsKeyWithValue(
35 | TemplateAnnotationIndex.KEY,
36 | "\\Instantiator\\Foobar\\Foobar._barInstantiator",
37 | value -> value.getFqn().equals("\\Instantiator\\Foobar\\Foobar._barInstantiator") && value.getParameterIndex() == 0 && value.getType() == TemplateAnnotationUsage.Type.FUNCTION_CLASS_STRING
38 | );
39 |
40 | assertIndexContainsKeyWithValue(
41 | TemplateAnnotationIndex.KEY,
42 | "\\instantiator",
43 | value -> value.getFqn().equals("\\instantiator") && value.getParameterIndex() == 0 && value.getType() == TemplateAnnotationUsage.Type.FUNCTION_CLASS_STRING
44 | );
45 |
46 | assertIndexContainsKeyWithValue(
47 | TemplateAnnotationIndex.KEY,
48 | "\\instantiatorPhpStan",
49 | value -> value.getFqn().equals("\\instantiatorPhpStan") && value.getParameterIndex() == 0 && value.getType() == TemplateAnnotationUsage.Type.FUNCTION_CLASS_STRING
50 | );
51 |
52 | assertIndexContainsKeyWithValue(
53 | TemplateAnnotationIndex.KEY,
54 | "\\instantiatorPhpStanAsObject",
55 | value -> value.getFqn().equals("\\instantiatorPhpStanAsObject") && value.getParameterIndex() == 1 && value.getType() == TemplateAnnotationUsage.Type.FUNCTION_CLASS_STRING
56 | );
57 | }
58 |
59 | public void testThatTemplateMethodWithConstructorTemplateIfInIndex() {
60 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Template\\MyTemplateImpl.getValue");
61 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Template\\MyTemplateImpl.getValueReturn");
62 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Template\\MyTemplateObject.getValue");
63 |
64 | assertIndexContainsKeyWithValue(
65 | TemplateAnnotationIndex.KEY,
66 | "\\Template\\MyTemplateImpl.getValue",
67 | value -> value.getType() == TemplateAnnotationUsage.Type.METHOD_TEMPLATE && "T".equals(value.getContext())
68 | );
69 |
70 | assertIndexContainsKeyWithValue(
71 | TemplateAnnotationIndex.KEY,
72 | "\\Template\\MyTemplateImpl.getValueReturn",
73 | value -> value.getType() == TemplateAnnotationUsage.Type.METHOD_TEMPLATE && "T".equals(value.getContext())
74 | );
75 |
76 | assertIndexContainsKeyWithValue(
77 | TemplateAnnotationIndex.KEY,
78 | "\\Template\\MyTemplateObject.getValue",
79 | value -> value.getType() == TemplateAnnotationUsage.Type.METHOD_TEMPLATE && "T".equals(value.getContext())
80 | );
81 | }
82 |
83 | public void testThatTemplateExtendsClassIsInIndex() {
84 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Extended\\Classes\\MyExtendsImpl");
85 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Extended\\Classes\\MyExtendsImplPsalm");
86 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Extended\\Classes\\MyExtendsImplPhpStan");
87 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Extended\\Classes\\MyExtendsImplUse");
88 | assertIndexContains(TemplateAnnotationIndex.KEY, "\\Extended\\Classes\\MyExtendsImplUseAlias");
89 |
90 | assertIndexContainsKeyWithValue(
91 | TemplateAnnotationIndex.KEY,
92 | "\\Extended\\Classes\\MyExtendsImpl",
93 | value -> value.getType() == TemplateAnnotationUsage.Type.EXTENDS && "\\App\\Foo\\Bar\\MyContainer::\\DateTime".equals(value.getContext())
94 | );
95 |
96 | assertIndexContainsKeyWithValue(
97 | TemplateAnnotationIndex.KEY,
98 | "\\Extended\\Classes\\MyExtendsImplPsalm",
99 | value -> value.getType() == TemplateAnnotationUsage.Type.EXTENDS && "\\App\\Foo\\Bar\\MyContainer::\\Extended\\Classes\\MyExtendsImplPalm".equals(value.getContext())
100 | );
101 |
102 | assertIndexContainsKeyWithValue(
103 | TemplateAnnotationIndex.KEY,
104 | "\\Extended\\Classes\\MyExtendsImplPhpStan",
105 | value -> value.getType() == TemplateAnnotationUsage.Type.EXTENDS && "\\App\\Foo\\Bar\\MyContainer::\\DateTime".equals(value.getContext())
106 | );
107 |
108 | assertIndexContainsKeyWithValue(
109 | TemplateAnnotationIndex.KEY,
110 | "\\Extended\\Classes\\MyExtendsImplUse",
111 | value -> value.getType() == TemplateAnnotationUsage.Type.EXTENDS && "\\App\\Foo\\Bar\\MyContainer::\\Extend\\Types\\Foobar".equals(value.getContext())
112 | );
113 |
114 | assertIndexContainsKeyWithValue(
115 | TemplateAnnotationIndex.KEY,
116 | "\\Extended\\Classes\\MyExtendsImplUseAlias",
117 | value -> value.getType() == TemplateAnnotationUsage.Type.EXTENDS && "\\App\\Foo\\Bar\\MyContainer::\\Extend\\Types\\Foobar".equals(value.getContext())
118 | );
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin, switch paths to Windows format before running java
129 | if $cygwin ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=$((i+1))
158 | done
159 | case $i in
160 | (0) set -- ;;
161 | (1) set -- "$args0" ;;
162 | (2) set -- "$args0" "$args1" ;;
163 | (3) set -- "$args0" "$args1" "$args2" ;;
164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=$(save "$@")
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
185 | cd "$(dirname "$0")"
186 | fi
187 |
188 | exec "$JAVACMD" "$@"
189 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 | de.espend.idea.php.generics
3 | PHPStan / Psalm / Generics
4 | 0.7.0
5 | espend_de
6 |
7 | Provide additional support for PHPStan, Psalm and Generics related PHP features
9 |
10 | !!! Work in progress !!!
11 |
12 | Supported syntax
13 |
14 |
18 |
19 | Features
20 |
21 | - class-string parameter inspection
22 | - Array-Types: Object-like arrays
23 | - Inspection for readonly access on property based on @psalm-readonly and @psalm-immutable tag
24 | - class-string template return type detection
25 | - PHPStan and Psalm quality tool support to show report result directly in IDE
26 |
27 |
28 | ]]>
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
41 |
42 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | com.intellij.modules.platform
110 | de.espend.idea.php.annotation
111 | org.jetbrains.plugins.phpstorm-remote-interpreter
112 |
113 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/utils/PhpTypeProviderUtil.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.utils;
2 |
3 |
4 | import com.intellij.openapi.util.text.StringUtil;
5 | import com.intellij.psi.PsiElement;
6 | import com.jetbrains.php.PhpIndex;
7 | import com.jetbrains.php.lang.psi.elements.*;
8 | import org.apache.commons.lang.StringUtils;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 |
12 | import java.util.ArrayList;
13 | import java.util.Collection;
14 | import java.util.Set;
15 |
16 | /**
17 | * @author Daniel Espendiller
18 | */
19 | public class PhpTypeProviderUtil {
20 |
21 | /**
22 | * Creates a signature for PhpType implementation which must be resolved inside 'getBySignature'
23 | *
24 | * eg. foo(MyClass::class) => "#F\\foo|#K#C\\Foo.class"
25 | *
26 | * foo($this->foo), foo('foobar')
27 | */
28 | @Nullable
29 | public static String getReferenceSignatureByIndex(@NotNull FunctionReference functionReference, int index) {
30 | PsiElement[] parameters = functionReference.getParameters();
31 | if(parameters.length < index) {
32 | return null;
33 | }
34 |
35 | PsiElement parameter = parameters[index];
36 |
37 | // we already have a string value
38 | if ((parameter instanceof StringLiteralExpression)) {
39 | String param = ((StringLiteralExpression)parameter).getContents();
40 | if (StringUtil.isNotEmpty(param)) {
41 | return param;
42 | }
43 |
44 | return null;
45 | }
46 |
47 | // whitelist here; we can also provide some more but think of performance
48 | // Service::NAME, $this->name and Entity::CLASS;
49 | if ((parameter instanceof ClassConstantReference || parameter instanceof FieldReference)) {
50 | String signature = ((PhpReference) parameter).getSignature();
51 | if (StringUtil.isNotEmpty(signature)) {
52 | return signature;
53 | }
54 | }
55 |
56 | return null;
57 | }
58 |
59 | public static Collection getReferenceSignatures(@NotNull FunctionReference functionReference) {
60 | Collection signatures = new ArrayList<>();
61 |
62 | for (PsiElement parameter : functionReference.getParameters()) {
63 | String signature = null;
64 |
65 | // we already have a string value
66 | if ((parameter instanceof StringLiteralExpression)) {
67 | String param = ((StringLiteralExpression)parameter).getContents();
68 | if (StringUtil.isNotEmpty(param)) {
69 | signature = param;
70 | }
71 | } else if ((parameter instanceof ClassConstantReference || parameter instanceof FieldReference)) {
72 | // whitelist here; we can also provide some more but think of performance
73 | // Service::NAME, $this->name and Entity::CLASS;
74 |
75 | String param = ((PhpReference) parameter).getSignature();
76 | if (StringUtil.isNotEmpty(param)) {
77 | signature = param;
78 | }
79 | }
80 |
81 | signatures.add(signature);
82 | }
83 |
84 | return signatures;
85 | }
86 |
87 | /**
88 | * Creates a signature for PhpType implementation which must be resolved inside 'getBySignature'
89 | *
90 | * eg. foo(MyClass::class) => "#F\\foo|#K#C\\Foo.class"
91 | *
92 | * foo($this->foo), foo('foobar')
93 | */
94 | @Nullable
95 | public static String getReferenceSignatureByFirstParameter(@NotNull FunctionReference functionReference, char trimKey) {
96 | String refSignature = functionReference.getSignature();
97 | if(StringUtil.isEmpty(refSignature)) {
98 | return null;
99 | }
100 |
101 | PsiElement[] parameters = functionReference.getParameters();
102 | if(parameters.length == 0) {
103 | return null;
104 | }
105 |
106 | PsiElement parameter = parameters[0];
107 |
108 | // we already have a string value
109 | if ((parameter instanceof StringLiteralExpression)) {
110 | String param = ((StringLiteralExpression)parameter).getContents();
111 | if (StringUtil.isNotEmpty(param)) {
112 | return refSignature + trimKey + param;
113 | }
114 |
115 | return null;
116 | }
117 |
118 | // whitelist here; we can also provide some more but think of performance
119 | // Service::NAME, $this->name and Entity::CLASS;
120 | if ((parameter instanceof ClassConstantReference || parameter instanceof FieldReference)) {
121 | String signature = ((PhpReference) parameter).getSignature();
122 | if (StringUtil.isNotEmpty(signature)) {
123 | return refSignature + trimKey + signature;
124 | }
125 | }
126 |
127 | return null;
128 | }
129 |
130 | /**
131 | * we can also pipe php references signatures and resolve them here
132 | * overwrite parameter to get string value
133 | */
134 | @Nullable
135 | public static String getResolvedParameter(@NotNull PhpIndex phpIndex, @NotNull String parameter) {
136 | return getResolvedParameter(phpIndex, parameter, null, 0);
137 | }
138 |
139 | /**
140 | * we can also pipe php references signatures and resolve them here
141 | * overwrite parameter to get string value
142 | */
143 | @Nullable
144 | public static String getResolvedParameter(@NotNull PhpIndex phpIndex, @NotNull String parameter, @Nullable Set visited, int depth) {
145 |
146 | // PHP 5.5 class constant: "Class\Foo::class"
147 | if(parameter.startsWith("#K#C")) {
148 | // PhpStorm9: #K#C\Class\Foo.class
149 | if(parameter.endsWith(".class")) {
150 | return StringUtils.stripStart(parameter.substring(4, parameter.length() - 6), "\\");
151 | }
152 | }
153 |
154 | // #K#C\Class\Foo.property
155 | // #K#C\Class\Foo.CONST
156 | if(parameter.startsWith("#")) {
157 |
158 | // get psi element from signature
159 | Collection extends PhpNamedElement> signTypes = phpIndex.getBySignature(parameter, visited, depth);
160 | if(signTypes.size() == 0) {
161 | return null;
162 | }
163 |
164 | // get string value
165 | parameter = GenericsUtil.getStringValue(signTypes.iterator().next());
166 | if(parameter == null) {
167 | return null;
168 | }
169 |
170 | }
171 |
172 | return parameter;
173 | }
174 |
175 | /**
176 | * We can have multiple types inside a TypeProvider; split them on "|" so that we dont get empty types
177 | *
178 | * #M#x#M#C\FooBar.get?doctrine.odm.mongodb.document_manager.getRepository|
179 | * #M#x#M#C\FooBar.get?doctrine.odm.mongodb.document_manager.getRepository
180 | */
181 | @NotNull
182 | public static Collection extends PhpNamedElement> getTypeSignature(@NotNull PhpIndex phpIndex, @NotNull String signature) {
183 |
184 | if (!signature.contains("|")) {
185 | return phpIndex.getBySignature(signature, null, 0);
186 | }
187 |
188 | Collection elements = new ArrayList<>();
189 | for (String s : signature.split("\\|")) {
190 | elements.addAll(phpIndex.getBySignature(s, null, 0));
191 | }
192 |
193 | return elements;
194 | }
195 | }
196 |
197 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/type/TemplateAnnotationTypeProvider.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.type;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.openapi.util.Key;
5 | import com.intellij.openapi.util.ModificationTracker;
6 | import com.intellij.psi.PsiElement;
7 | import com.intellij.psi.search.GlobalSearchScope;
8 | import com.intellij.psi.util.CachedValue;
9 | import com.intellij.psi.util.CachedValueProvider;
10 | import com.intellij.psi.util.CachedValuesManager;
11 | import com.intellij.util.indexing.FileBasedIndex;
12 | import com.jetbrains.php.PhpIndex;
13 | import com.jetbrains.php.lang.psi.elements.FunctionReference;
14 | import com.jetbrains.php.lang.psi.elements.Method;
15 | import com.jetbrains.php.lang.psi.elements.PhpNamedElement;
16 | import com.jetbrains.php.lang.psi.resolve.types.PhpType;
17 | import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4;
18 | import de.espend.idea.php.generics.indexer.TemplateAnnotationIndex;
19 | import de.espend.idea.php.generics.indexer.dict.TemplateAnnotationUsage;
20 | import de.espend.idea.php.generics.utils.PhpTypeProviderUtil;
21 | import org.apache.commons.lang.StringUtils;
22 | import org.jetbrains.annotations.NotNull;
23 | import org.jetbrains.annotations.Nullable;
24 |
25 | import java.util.*;
26 | import java.util.regex.Matcher;
27 | import java.util.regex.Pattern;
28 |
29 | /**
30 | * @author Daniel Espendiller
31 | */
32 | public class TemplateAnnotationTypeProvider implements PhpTypeProvider4 {
33 | private static final Key>>> PHP_GENERICS_TEMPLATES = new Key<>("PHP_GENERICS_TEMPLATES");
34 |
35 | /**
36 | * #M#C\Class\Foo.getMethod
37 | */
38 | private static final Pattern METHOD_CALL_SIGNATURE_MATCHER = Pattern.compile("#M#C([^.]+)\\.(.*)");
39 |
40 | /**
41 | * Separator for the parameter types
42 | */
43 | private static final char PARAMETER_SEPARATOR = '\u0199';
44 |
45 | @Override
46 | public char getKey() {
47 | return '\u0197';
48 | }
49 |
50 | @Nullable
51 | @Override
52 | public PhpType getType(PsiElement psiElement) {
53 | if (psiElement instanceof FunctionReference) {
54 | String subject = ((FunctionReference) psiElement).getSignature();
55 | String parameters = StringUtils.join(PhpTypeProviderUtil.getReferenceSignatures((FunctionReference) psiElement), PARAMETER_SEPARATOR);
56 |
57 | // done also by PhpStorm; is this suitable? reduce parameters maybe to limit to one on longer values?
58 | if (subject.length() <= 200 && parameters.length() <= 300) {
59 | return new PhpType().add("#" + this.getKey() + subject + '\u0198' + parameters);
60 | } else if(subject.length() <= 200) {
61 | // fallback on long parameter; to support at least some other features
62 | return new PhpType().add("#" + this.getKey() + subject + '\u0198');
63 | }
64 | }
65 |
66 | return null;
67 | }
68 |
69 | @Nullable
70 | @Override
71 | public PhpType complete(String s, Project project) {
72 | if (!s.startsWith("#" + this.getKey())) {
73 | return null;
74 | }
75 |
76 | Collection types = new HashSet<>();
77 |
78 | String[] subjectAndParameters = s.substring(2).split(String.valueOf('\u0198'));
79 |
80 | if (subjectAndParameters.length == 0) {
81 | return null;
82 | }
83 |
84 | // "@template for parameters"
85 | // split for "subject" and its "parameters"
86 | // PhpStorm split on multiple types too
87 | String[] signatures = subjectAndParameters[0].split("\\|");
88 | for (String signature : signatures) {
89 |
90 | for (PhpNamedElement phpNamedElement : PhpIndex.getInstance(project).getBySignature(signature)) {
91 | String fqn = phpNamedElement.getFQN();
92 |
93 | Collection templateAnnotationUsages = getTemplateAnnotationUsagesMap(project, fqn);
94 | if (templateAnnotationUsages.size() == 0) {
95 | continue;
96 | }
97 |
98 | if (subjectAndParameters.length >= 2) {
99 | visitParameterTypes(types, subjectAndParameters[1], templateAnnotationUsages);
100 | }
101 |
102 | visitTemplateAnnotatedMethod(project, types, signature, phpNamedElement, templateAnnotationUsages);
103 | }
104 | }
105 |
106 | if (types.size() == 0) {
107 | return null;
108 | }
109 |
110 | PhpType phpType = new PhpType();
111 | types.forEach(phpType::add);
112 | return phpType;
113 | }
114 |
115 | @NotNull
116 | private static Collection getTemplateAnnotationUsagesMap(@NotNull Project project, @NotNull String fqn) {
117 | Map> map = getTemplateAnnotationUsagesMap(project);
118 | return map.getOrDefault(fqn, Collections.emptyList());
119 | }
120 |
121 | @NotNull
122 | private static Map> getTemplateAnnotationUsagesMap(@NotNull Project project) {
123 | return CachedValuesManager.getManager(project).getCachedValue(project, PHP_GENERICS_TEMPLATES, () -> {
124 | Map> map = new HashMap<>();
125 |
126 | FileBasedIndex instance = FileBasedIndex.getInstance();
127 | GlobalSearchScope scope = PhpIndex.getInstance(project).getSearchScope();
128 |
129 | instance.processAllKeys(TemplateAnnotationIndex.KEY, (key) -> {
130 | map.putIfAbsent(key, new HashSet<>());
131 | map.get(key).addAll(instance.getValues(TemplateAnnotationIndex.KEY, key, scope));
132 | return true;
133 | }, project);
134 |
135 | return CachedValueProvider.Result.create(map, getModificationTracker(project));
136 | }, false);
137 | }
138 |
139 | @NotNull
140 | private static ModificationTracker getModificationTracker(@NotNull Project project) {
141 | return () -> FileBasedIndex.getInstance().getIndexModificationStamp(TemplateAnnotationIndex.KEY, project);
142 | }
143 |
144 | /**
145 | * Supports "@extends" and "@implements"
146 | *
147 | * - "@extends \Extended\Implementations\MyContainer<\Extended\Implementations\Foobar>"
148 | */
149 | private void visitTemplateAnnotatedMethod(@NotNull Project project, @NotNull Collection types, @NotNull String signature, @NotNull PhpNamedElement phpNamedElement, @NotNull Collection usages) {
150 | if (!(phpNamedElement instanceof Method)) {
151 | return;
152 | }
153 |
154 | for (TemplateAnnotationUsage usage : usages) {
155 | if (usage.getType() == TemplateAnnotationUsage.Type.METHOD_TEMPLATE) {
156 | // it class does not implement the method we got into "parent" class; here we get the orgin class name
157 | Matcher matcher = METHOD_CALL_SIGNATURE_MATCHER.matcher(signature);
158 | if (!matcher.find()) {
159 | continue;
160 | }
161 |
162 | // Find class "@extends" tag and the origin class
163 | String group = matcher.group(1);
164 | for (TemplateAnnotationUsage origin : getTemplateAnnotationUsagesMap(project, group)) {
165 | if (origin.getType() == TemplateAnnotationUsage.Type.EXTENDS) {
166 | String context = origin.getContext();
167 | if (context != null) {
168 | String[] split = context.split("::");
169 | if (split.length > 1) {
170 | types.add("#" + this.getKey() + "#K#C" + split[1] + ".class");
171 | }
172 | }
173 | }
174 | }
175 | }
176 | }
177 | }
178 |
179 | private void visitParameterTypes(@NotNull Collection types, @NotNull String subjectAndParameter, @NotNull Collection templateAnnotationUsages) {
180 | List parameters = Arrays.asList(subjectAndParameter.split(String.valueOf(PARAMETER_SEPARATOR)));
181 | for (TemplateAnnotationUsage usage : templateAnnotationUsages) {
182 | Integer parameterIndex = usage.getParameterIndex();
183 | if (parameterIndex == null) {
184 | continue;
185 | }
186 |
187 | if (parameters.isEmpty()) {
188 | return;
189 | }
190 |
191 | String s1 = parameters.get(parameterIndex);
192 | if (s1 == null) {
193 | return;
194 | }
195 |
196 | types.add("#" + this.getKey() + s1);
197 | }
198 | }
199 |
200 | @Override
201 | public Collection extends PhpNamedElement> getBySignature(String expression, Set set, int i, Project project) {
202 | PhpIndex phpIndex = PhpIndex.getInstance(project);
203 |
204 | String resolvedParameter = PhpTypeProviderUtil.getResolvedParameter(phpIndex, expression);
205 | if(resolvedParameter == null) {
206 | return null;
207 | }
208 |
209 | return phpIndex.getAnyByFQN(resolvedParameter);
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/indexer/TemplateAnnotationIndex.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.indexer;
2 |
3 | import com.intellij.psi.PsiElement;
4 | import com.intellij.psi.PsiFile;
5 | import com.intellij.psi.PsiRecursiveElementWalkingVisitor;
6 | import com.intellij.util.indexing.*;
7 | import com.intellij.util.io.DataExternalizer;
8 | import com.intellij.util.io.EnumeratorStringDescriptor;
9 | import com.intellij.util.io.KeyDescriptor;
10 | import com.jetbrains.php.lang.PhpFileType;
11 | import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
12 | import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
13 | import com.jetbrains.php.lang.psi.PhpFile;
14 | import com.jetbrains.php.lang.psi.elements.Function;
15 | import com.jetbrains.php.lang.psi.elements.Method;
16 | import com.jetbrains.php.lang.psi.elements.Parameter;
17 | import com.jetbrains.php.lang.psi.elements.PhpClass;
18 | import de.espend.idea.php.annotation.util.AnnotationUtil;
19 | import de.espend.idea.php.generics.indexer.dict.TemplateAnnotationUsage;
20 | import de.espend.idea.php.generics.indexer.externalizer.ObjectStreamDataExternalizer;
21 | import de.espend.idea.php.generics.utils.GenericsUtil;
22 | import org.apache.commons.lang.StringUtils;
23 | import org.jetbrains.annotations.NotNull;
24 | import org.jetbrains.annotations.Nullable;
25 |
26 | import java.util.HashMap;
27 | import java.util.Map;
28 | import java.util.regex.Matcher;
29 | import java.util.regex.Pattern;
30 |
31 | /**
32 | * @author Daniel Espendiller
33 | */
34 | public class TemplateAnnotationIndex extends FileBasedIndexExtension {
35 | public static final ID KEY = ID.create("de.espend.idea.php.generics.templates");
36 | private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor();
37 | private static final ObjectStreamDataExternalizer EXTERNALIZER = new ObjectStreamDataExternalizer<>();
38 |
39 | @NotNull
40 | @Override
41 | public ID getName() {
42 | return KEY;
43 | }
44 |
45 | @NotNull
46 | @Override
47 | public DataIndexer getIndexer() {
48 | return inputData -> {
49 | final Map map = new HashMap<>();
50 |
51 | PsiFile psiFile = inputData.getPsiFile();
52 | if (!(psiFile instanceof PhpFile)) {
53 | return map;
54 | }
55 |
56 | if (!AnnotationUtil.isValidForIndex(inputData)) {
57 | return map;
58 | }
59 |
60 | psiFile.accept(new MyPsiRecursiveElementWalkingVisitor(map));
61 |
62 | return map;
63 | };
64 | }
65 |
66 | @NotNull
67 | @Override
68 | public KeyDescriptor getKeyDescriptor() {
69 | return myKeyDescriptor;
70 | }
71 |
72 | @NotNull
73 | @Override
74 | public DataExternalizer getValueExternalizer() {
75 | return EXTERNALIZER;
76 | }
77 |
78 | @Override
79 | public int getVersion() {
80 | return 3;
81 | }
82 |
83 | @NotNull
84 | @Override
85 | public FileBasedIndex.InputFilter getInputFilter() {
86 | return virtualFile -> virtualFile.getFileType() == PhpFileType.INSTANCE;
87 | }
88 |
89 | @Override
90 | public boolean dependsOnFileContent() {
91 | return true;
92 | }
93 |
94 | private static class MyPsiRecursiveElementWalkingVisitor extends PsiRecursiveElementWalkingVisitor {
95 |
96 | /**
97 | * Matches: "\App\Foo\Bar\MyContainer<\DateTime>"
98 | */
99 | private static final Pattern CLASS_EXTENDS_MATCHER = Pattern.compile("\\s*([^<]+)\\s*<\\s*([^>]+)\\s*>");
100 |
101 | private final Map map;
102 |
103 | private MyPsiRecursiveElementWalkingVisitor(Map map) {
104 | this.map = map;
105 | }
106 |
107 | @Override
108 | public void visitElement(@NotNull PsiElement element) {
109 | if (element instanceof PhpClass) {
110 | visitPhpClass((PhpClass) element);
111 | } else if (element instanceof Function) {
112 | visitPhpFunctionOrMethod((Function) element);
113 | }
114 |
115 | super.visitElement(element);
116 | }
117 |
118 | private void visitPhpClass(@NotNull PhpClass phpClass) {
119 | String fqn = phpClass.getFQN();
120 | if(!fqn.startsWith("\\")) {
121 | fqn = "\\" + fqn;
122 | }
123 |
124 | PhpDocComment phpDocComment = phpClass.getDocComment();
125 | if (phpDocComment != null) {
126 | Map useImportMap = null;
127 | for (PhpDocTag phpDocTag : GenericsUtil.getTagElementsByNameForAllFrameworks(phpDocComment, "extends")) {
128 | String tagValue = phpDocTag.getTagValue();
129 |
130 | Matcher matcher = CLASS_EXTENDS_MATCHER.matcher(tagValue);
131 | if (!matcher.find()) {
132 | continue;
133 | }
134 |
135 | String extendsClass = matcher.group(1);
136 | String type = matcher.group(2);
137 |
138 | // init the imports scope; to be only loaded once
139 | if (useImportMap == null) {
140 | useImportMap = AnnotationUtil.getUseImportMap(phpDocComment);
141 | }
142 |
143 | // resolve the class name based on the scope of namespace and use statement
144 | // eg: "@template BarAlias\MyContainer" we need global namespaces starting with "\"
145 | extendsClass = GenericsUtil.getFqnClassNameFromScope(fqn, extendsClass, useImportMap);
146 | type = GenericsUtil.getFqnClassNameFromScope(fqn, type, useImportMap);
147 |
148 | map.put(fqn, new TemplateAnnotationUsage(
149 | fqn,
150 | TemplateAnnotationUsage.Type.EXTENDS,
151 | 0,
152 | extendsClass + "::" + type
153 | ));
154 | }
155 | }
156 |
157 | }
158 |
159 | private void visitPhpFunctionOrMethod(@NotNull Function function) {
160 | PhpDocComment phpDocComment = function.getDocComment();
161 | if (phpDocComment == null) {
162 | return;
163 | }
164 |
165 | /*
166 | *
167 | */
168 | for (PhpDocTag phpDocTag : GenericsUtil.getTagElementsByNameForAllFrameworks(phpDocComment, "template")) {
169 | // @template T
170 | String templateName = extractTemplateName(phpDocTag);
171 | if (templateName == null) {
172 | continue;
173 | }
174 |
175 | // return doctag must match: "@return T"
176 | if (!hasReturnTypeTemplate(phpDocComment, templateName)) {
177 | continue;
178 | }
179 |
180 | // get possible tags
181 | for (PhpDocTag docTag : GenericsUtil.getTagElementsByNameForAllFrameworks(phpDocComment, "param")) {
182 | String psalmParamTag = docTag.getTagValue();
183 | Pattern pattern = Pattern.compile("class-string<" + Pattern.quote(templateName) + ">.*\\$([\\w-]+)");
184 |
185 | Matcher matcher = pattern.matcher(psalmParamTag);
186 | if (!matcher.find()) {
187 | continue;
188 | }
189 |
190 | String parameterName = matcher.group(1);
191 | Parameter[] parameters = function.getParameters();
192 |
193 | for (int i = 0; i < parameters.length; i++) {
194 | Parameter parameter = parameters[i];
195 | String name = parameter.getName();
196 | if (name.equalsIgnoreCase(parameterName)) {
197 | String fqn = function.getFQN();
198 |
199 | map.put(fqn, new TemplateAnnotationUsage(fqn, TemplateAnnotationUsage.Type.FUNCTION_CLASS_STRING, i));
200 | return;
201 | }
202 | }
203 | }
204 | }
205 |
206 | /*
207 | *
208 | */
209 | if (function instanceof Method) {
210 | for (String docTagValue : GenericsUtil.getReturnTypeTagValues(phpDocComment)) {
211 | String templateName = extractTemplateName(docTagValue);
212 | if (templateName == null) {
213 | continue;
214 | }
215 |
216 | PhpClass containingClass = ((Method) function).getContainingClass();
217 | if (containingClass == null) {
218 | continue;
219 | }
220 |
221 | PhpDocComment docComment = containingClass.getDocComment();
222 | if (docComment == null) {
223 | continue;
224 | }
225 |
226 | for (PhpDocTag template : GenericsUtil.getTagElementsByNameForAllFrameworks(docComment, "template")) {
227 | String templateNameClassLevel = extractTemplateName(template);
228 | if (StringUtils.isBlank(templateNameClassLevel)) {
229 | continue;
230 | }
231 |
232 | if (templateNameClassLevel.equals(templateName)) {
233 | String fqn = function.getFQN();
234 | map.put(fqn, new TemplateAnnotationUsage(fqn, TemplateAnnotationUsage.Type.METHOD_TEMPLATE, 0, templateName));
235 | }
236 | }
237 | }
238 | }
239 | }
240 |
241 | /**
242 | * Extract the "T"
243 | *
244 | * "T"
245 | * "T as object"
246 | */
247 | @Nullable
248 | private static String extractTemplateName(@NotNull PhpDocTag phpDocTag) {
249 | String templateTagValue = phpDocTag.getTagValue();
250 | if (StringUtils.isBlank(templateTagValue)) {
251 | return null;
252 | }
253 |
254 | return extractTemplateName(templateTagValue);
255 | }
256 |
257 | /**
258 | * Extract the "T"
259 | *
260 | * "T"
261 | * "T as object"
262 | */
263 | @Nullable
264 | private static String extractTemplateName(@NotNull String tagValue) {
265 | Matcher matcher = Pattern.compile("^([\\w]+)\\s*").matcher(tagValue);
266 | if (!matcher.find()) {
267 | return null;
268 | }
269 |
270 | return StringUtils.trim(matcher.group(1));
271 | }
272 |
273 | /**
274 | * For for the given template name as a return value
275 | *
276 | * "@return T"
277 | * "@psalm-return T"
278 | * "@return T as object"
279 | */
280 | private boolean hasReturnTypeTemplate(@NotNull PhpDocComment phpDocComment, @NotNull String templateName) {
281 | return GenericsUtil.getReturnTypeTagValues(phpDocComment)
282 | .stream()
283 | .map(MyPsiRecursiveElementWalkingVisitor::extractTemplateName)
284 | .anyMatch(templateName::equals);
285 | }
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/src/main/java/de/espend/idea/php/generics/utils/GenericsUtil.java:
--------------------------------------------------------------------------------
1 | package de.espend.idea.php.generics.utils;
2 |
3 | import com.intellij.openapi.project.Project;
4 | import com.intellij.psi.PsiElement;
5 | import com.intellij.psi.PsiReference;
6 | import com.jetbrains.php.PhpIndex;
7 | import com.jetbrains.php.codeInsight.PhpCodeInsightUtil;
8 | import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
9 | import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
10 | import com.jetbrains.php.lang.psi.elements.*;
11 | import com.jetbrains.php.lang.psi.resolve.types.PhpType;
12 | import de.espend.idea.php.generics.dict.ParameterArrayType;
13 | import org.apache.commons.lang.StringUtils;
14 | import org.jetbrains.annotations.NotNull;
15 | import org.jetbrains.annotations.Nullable;
16 |
17 | import java.util.*;
18 | import java.util.regex.Matcher;
19 | import java.util.regex.Pattern;
20 | import java.util.stream.Collectors;
21 | import java.util.stream.Stream;
22 |
23 | public class GenericsUtil {
24 | public static boolean isGenericsClass(@NotNull PhpClass phpClass) {
25 | PhpDocComment phpDocComment = phpClass.getDocComment();
26 | if(phpDocComment != null) {
27 | // "@template T"
28 | // "@psalm-template Foo"
29 | for (PhpDocTag phpDocTag : getTagElementsByNameForAllFrameworks(phpDocComment, "template")) {
30 | String tagValue = phpDocTag.getTagValue();
31 | if (StringUtils.isNotBlank(tagValue) && tagValue.matches("\\w+")) {
32 | return true;
33 | }
34 | }
35 | }
36 |
37 | return false;
38 | }
39 |
40 | @Nullable
41 | public static String getExpectedParameterInstanceOf(@NotNull PsiElement psiElement) {
42 | PsiElement parameterList = psiElement.getParent();
43 | if (!(parameterList instanceof ParameterList)) {
44 | return null;
45 | }
46 |
47 | PsiElement functionReference = parameterList.getParent();
48 | if (!(functionReference instanceof FunctionReference)) {
49 | return null;
50 | }
51 |
52 | Integer currentParameterIndex = getCurrentParameterIndex(psiElement);
53 | if (currentParameterIndex == null) {
54 | return null;
55 | }
56 |
57 | PsiElement resolve = ((FunctionReference) functionReference).resolve();
58 | if (!(resolve instanceof Function)) {
59 | return null;
60 | }
61 |
62 | Parameter[] parameters = ((Function) resolve).getParameters();
63 | if (parameters.length <= currentParameterIndex) {
64 | return null;
65 | }
66 |
67 | PhpDocComment docComment = ((Function) resolve).getDocComment();
68 | if (docComment == null) {
69 | return null;
70 | }
71 |
72 | Map asInstances = new HashMap<>();
73 |
74 | // workarounds for inconsistently psi structure
75 | // https://youtrack.jetbrains.com/issue/WI-47644
76 | for (PhpDocTag template : getTagElementsByNameForAllFrameworks(docComment, "template")) {
77 | Matcher matcher = Pattern.compile("([\\w_-]+)\\s+as\\s+([\\w_\\\\-]+)", Pattern.MULTILINE).matcher(template.getText());
78 | if (!matcher.find()) {
79 | continue;
80 | }
81 |
82 | asInstances.put(matcher.group(1), matcher.group(2));
83 | }
84 |
85 | String instance = null;
86 | for (PhpDocTag phpDocParamTag : getTagElementsByNameForAllFrameworks(docComment, "param")) {
87 | String tagText = phpDocParamTag.getText();
88 | if (!tagText.contains("$" + parameters[currentParameterIndex].getName())) {
89 | continue;
90 | }
91 |
92 | Matcher matcher = Pattern.compile("\\s*([\\w_-]+)::class\\s*", Pattern.MULTILINE).matcher(tagText);
93 | if (!matcher.find()) {
94 | continue;
95 | }
96 |
97 | String group = matcher.group(1);
98 | if (!asInstances.containsKey(group)) {
99 | continue;
100 | }
101 |
102 | instance = asInstances.get(group);
103 | break;
104 | }
105 |
106 | if (instance == null) {
107 | return null;
108 | }
109 |
110 | Map useImportMap = getUseImportMap(docComment);
111 | if (useImportMap.containsKey(instance)) {
112 | return StringUtils.stripStart(useImportMap.get(instance), "\\");
113 | }
114 |
115 | return instance;
116 | }
117 |
118 | /**
119 | * - "@return array{optional?: string, bar: int}"
120 | * - "@return array{foo: string, bar: int}"
121 | * - "@psalm-param array{foo: string, bar: int}"
122 | */
123 | @NotNull
124 | public static Collection getReturnArrayTypes(@NotNull PhpNamedElement phpNamedElement) {
125 | PhpDocComment docComment = phpNamedElement.getDocComment();
126 | if (docComment == null) {
127 | return Collections.emptyList();
128 | }
129 |
130 | Collection types = new ArrayList<>();
131 |
132 | // workaround for invalid tags lexer on PhpStorm side
133 | for (PhpDocTag phpDocTag : getTagElementsByNameForAllFrameworks(docComment, "return")) {
134 | String text = phpDocTag.getText();
135 | Matcher arrayElementsMatcher = Pattern.compile("array\\s*\\{(.*)}\\s*", Pattern.MULTILINE).matcher(text);
136 | if (arrayElementsMatcher.find()) {
137 | String group = arrayElementsMatcher.group(1);
138 | types.addAll(GenericsUtil.getParameterArrayTypes(group, phpDocTag));
139 | }
140 | }
141 |
142 | return types;
143 | }
144 |
145 | /**
146 | * - "@return array{optional?: string, bar: int}"
147 | * - "@return array{foo: string, bar: int}"
148 | * - "@return array{foo: string, bar: int}"
149 | * - "@psalm-param array{foo: Foo, ?bar: int}"
150 | * - "@param array{foo: Foo, ?bar: int} $options"
151 | */
152 | @NotNull
153 | public static Collection getParameterArrayTypes(@NotNull String content, @NotNull String parameter, @NotNull PsiElement context) {
154 | Matcher parameterNameMatcher = Pattern.compile(".*\\$([\\w_-]+)\\s*$", Pattern.MULTILINE).matcher(content);
155 | if (!parameterNameMatcher.find()) {
156 | return Collections.emptyList();
157 | }
158 |
159 | String group = parameterNameMatcher.group(1);
160 | if (!parameter.equalsIgnoreCase(group)) {
161 | return Collections.emptyList();
162 | }
163 |
164 | // array{foo: string, bar: int}
165 | Matcher arrayElementsMatcher = Pattern.compile("array\\s*\\{(.*)}\\s*", Pattern.MULTILINE).matcher(content);
166 | if (!arrayElementsMatcher.find()) {
167 | return Collections.emptyList();
168 | }
169 |
170 | return getParameterArrayTypes(arrayElementsMatcher.group(1), context);
171 | }
172 |
173 | @NotNull
174 | private static Collection getParameterArrayTypes(@NotNull PhpDocComment phpDocComment, @NotNull String parameterName) {
175 | Collection vars = new ArrayList<>();
176 |
177 | for (PhpDocTag phpDocTag : getTagElementsByNameForAllFrameworks(phpDocComment, "param")) {
178 | String tagValue = phpDocTag.getTagValue();
179 | vars.addAll(GenericsUtil.getParameterArrayTypes(tagValue, parameterName, phpDocTag));
180 | }
181 |
182 | // we need a workaround for "@param" as the lexer strips it all of after "array{"
183 | for (PhpDocTag phpDocTag : phpDocComment.getTagElementsByName("@param")) {
184 | // {foobar2: string} $foobar
185 | String tagValue = phpDocTag.getTagValue();
186 |
187 | // extract the parameter name $foobar
188 | Matcher parameterNameMatcher = Pattern.compile(".*\\$([\\w_-]+)\\s*$", Pattern.MULTILINE).matcher(tagValue);
189 | if (!parameterNameMatcher.find()) {
190 | continue;
191 | }
192 |
193 | // @param array{foobar2: string}
194 | String text = phpDocTag.getText();
195 |
196 | // try to build a valid string; make in as error prone safe as possible; we need provide as on "@psalm-param":
197 | // array{foobar2: string} $foobar
198 | String content = text.replaceAll("\\s*@param\\s*", "") + " $" + parameterNameMatcher.group(1);
199 |
200 | vars.addAll(GenericsUtil.getParameterArrayTypes(content, parameterName, phpDocTag));
201 | }
202 |
203 | return vars;
204 | }
205 |
206 | /**
207 | * - "@return array{optional?: string, bar: int}"
208 | * - "@return array{foo: string, bar: int}"
209 | * - "@return array{foo: string, bar: int}"
210 | * - "@psalm-param array{foo: Foo, ?bar: int}"
211 | * - "@param array{foo: Foo, ?bar: int} $options"
212 | */
213 | @NotNull
214 | private static Collection getParameterArrayTypes(@NotNull String array, @NotNull PsiElement context) {
215 | Collection parameters = new ArrayList<>();
216 |
217 | for (String s : array.split(",")) {
218 | String trim = StringUtils.trim(s);
219 | String[] split = trim.split(":");
220 |
221 | if(split.length != 2) {
222 | continue;
223 | }
224 |
225 | // @TODO: class resolve
226 | Set types = Arrays.stream(split[1].split("\\|"))
227 | .map(StringUtils::trim)
228 | .collect(Collectors.toSet());
229 |
230 | boolean isOptional = split[0].startsWith("?") || split[0].endsWith("?");
231 |
232 | parameters.add(new ParameterArrayType(
233 | isOptional ? StringUtils.strip(split[0], "?") : split[0],
234 | types,
235 | isOptional,
236 | context
237 | ));
238 | }
239 |
240 | return parameters;
241 | }
242 |
243 | /**
244 | * Resolve the given parameter to find possible psalm docs recursively
245 | *
246 | * $foo->foo([])
247 | *
248 | * TODO: method search in recursion
249 | */
250 | @NotNull
251 | public static Collection getTypesForParameter(@NotNull PsiElement psiElement) {
252 | PsiElement parent = psiElement.getParent();
253 |
254 | if (parent instanceof ParameterList) {
255 | PsiElement functionReference = parent.getParent();
256 | if (functionReference instanceof FunctionReference) {
257 | PsiElement resolve = ((FunctionReference) functionReference).resolve();
258 |
259 | if (resolve instanceof Function) {
260 | Parameter[] functionParameters = ((Function) resolve).getParameters();
261 |
262 | int currentParameterIndex = PhpElementsUtil.getCurrentParameterIndex((ParameterList) parent, psiElement);
263 | if (currentParameterIndex >= 0 && functionParameters.length - 1 >= currentParameterIndex) {
264 | String name = functionParameters[currentParameterIndex].getName();
265 | PhpDocComment docComment = ((Function) resolve).getDocComment();
266 |
267 | if (docComment != null) {
268 | return GenericsUtil.getParameterArrayTypes(docComment, name);
269 | }
270 | }
271 | }
272 | }
273 | }
274 |
275 | return Collections.emptyList();
276 | }
277 |
278 | @Nullable
279 | private static Integer getCurrentParameterIndex(PsiElement parameter) {
280 | PsiElement parameterList = parameter.getContext();
281 | if(!(parameterList instanceof ParameterList)) {
282 | return null;
283 | }
284 |
285 | PsiElement[] parameters = ((ParameterList) parameterList).getParameters();
286 |
287 | int i;
288 | for(i = 0; i < parameters.length; i = i + 1) {
289 | if(parameters[i].equals(parameter)) {
290 | return i;
291 | }
292 | }
293 |
294 | return null;
295 | }
296 |
297 | /*
298 | * Collect file use imports and resolve alias with their class name
299 | *
300 | * @param PhpDocComment current doc scope
301 | * @return map with class names as key and fqn on value
302 | */
303 | @NotNull
304 | private static Map getUseImportMap(@Nullable PhpDocComment phpDocComment) {
305 | if(phpDocComment == null) {
306 | return Collections.emptyMap();
307 | }
308 |
309 | PhpPsiElement scope = PhpCodeInsightUtil.findScopeForUseOperator(phpDocComment);
310 | if(scope == null) {
311 | return Collections.emptyMap();
312 | }
313 |
314 | Map useImports = new HashMap<>();
315 |
316 | for (PhpUseList phpUseList : PhpCodeInsightUtil.collectImports(scope)) {
317 | for(PhpUse phpUse : phpUseList.getDeclarations()) {
318 | String alias = phpUse.getAliasName();
319 | if (alias != null) {
320 | useImports.put(alias, phpUse.getFQN());
321 | } else {
322 | useImports.put(phpUse.getName(), phpUse.getFQN());
323 | }
324 | }
325 | }
326 |
327 | return useImports;
328 | }
329 |
330 | /**
331 | * Resolve string definition in a recursive way
332 | *
333 | * $foo = Foo::class
334 | * $this->foo = Foo::class
335 | * $this->foo1 = $this->foo
336 | */
337 | @Nullable
338 | public static String getStringValue(@Nullable PsiElement psiElement) {
339 | return getStringValue(psiElement, 0);
340 | }
341 |
342 | @Nullable
343 | private static String getStringValue(@Nullable PsiElement psiElement, int depth) {
344 | if(psiElement == null || ++depth > 5) {
345 | return null;
346 | }
347 |
348 | if(psiElement instanceof StringLiteralExpression) {
349 | String resolvedString = ((StringLiteralExpression) psiElement).getContents();
350 | if(StringUtils.isEmpty(resolvedString)) {
351 | return null;
352 | }
353 |
354 | return resolvedString;
355 | } else if(psiElement instanceof Field) {
356 | return getStringValue(((Field) psiElement).getDefaultValue(), depth);
357 | } else if(psiElement instanceof ClassConstantReference && "class".equals(((ClassConstantReference) psiElement).getName())) {
358 | // Foobar::class
359 | return getClassConstantPhpFqn((ClassConstantReference) psiElement);
360 | } else if(psiElement instanceof PhpReference) {
361 | PsiReference psiReference = psiElement.getReference();
362 | if(psiReference == null) {
363 | return null;
364 | }
365 |
366 | PsiElement ref = psiReference.resolve();
367 | if(ref instanceof PhpReference) {
368 | return getStringValue(psiElement, depth);
369 | }
370 |
371 | if(ref instanceof Field) {
372 | return getStringValue(((Field) ref).getDefaultValue());
373 | }
374 | }
375 |
376 | return null;
377 | }
378 |
379 |
380 | /**
381 | * Foo::class to its class fqn include namespace
382 | */
383 | public static String getClassConstantPhpFqn(@NotNull ClassConstantReference classConstant) {
384 | PhpExpression classReference = classConstant.getClassReference();
385 | if(!(classReference instanceof PhpReference)) {
386 | return null;
387 | }
388 |
389 | String typeName = ((PhpReference) classReference).getFQN();
390 | return StringUtils.isNotBlank(typeName) ? StringUtils.stripStart(typeName, "\\") : null;
391 | }
392 |
393 |
394 | /**
395 | * @param subjectClass eg DateTime
396 | * @param expectedClass eg DateTimeInterface
397 | */
398 | public static boolean isInstanceOf(@NotNull PhpClass subjectClass, @NotNull PhpClass expectedClass) {
399 | return new PhpType().add(expectedClass).isConvertibleFrom(new PhpType().add(subjectClass), PhpIndex.getInstance(subjectClass.getProject()));
400 | }
401 |
402 | /**
403 | * @param subjectClass eg DateTime
404 | * @param expectedClass eg DateTimeInterface
405 | */
406 | public static boolean isInstanceOf(@NotNull PhpClass subjectClass, @NotNull String expectedClass) {
407 | return new PhpType().add(expectedClass).isConvertibleFrom(new PhpType().add(subjectClass), PhpIndex.getInstance(subjectClass.getProject()));
408 | }
409 |
410 | /**
411 | * @param subjectClass eg DateTime
412 | * @param expectedClass eg DateTimeInterface
413 | */
414 | public static boolean isInstanceOf(@NotNull Project project, @NotNull String subjectClass, @NotNull String expectedClass) {
415 | return new PhpType().add(expectedClass).isConvertibleFrom(new PhpType().add(subjectClass), PhpIndex.getInstance(project));
416 | }
417 |
418 | @Nullable
419 | static public PhpClass findClass(Project project, @NotNull String className) {
420 | Collection phpClasses = PhpIndex.getInstance(project).getAnyByFQN(className);
421 | return phpClasses.size() == 0 ? null : phpClasses.iterator().next();
422 | }
423 |
424 | @NotNull
425 | public static PhpDocTag[] getTagElementsByNameForAllFrameworks(@NotNull PhpDocComment phpDocComment, @NotNull String parameterName) {
426 | return Stream.of(
427 | phpDocComment.getTagElementsByName("@psalm-" + parameterName),
428 | phpDocComment.getTagElementsByName("@" + parameterName),
429 | phpDocComment.getTagElementsByName("@phpstan-" + parameterName)
430 | ).flatMap(Stream::of).toArray(PhpDocTag[]::new);
431 | }
432 |
433 | public static Collection getReturnTypeTagValues(@NotNull PhpDocComment phpDocComment) {
434 | String[] strings = {
435 | "@psalm-",
436 | "@",
437 | "@phpstan-"
438 | };
439 |
440 | Collection returns = new HashSet<>();
441 | for (String prefix : strings) {
442 | for (PhpDocTag phpDocTag : phpDocComment.getTagElementsByName(prefix + "return")) {
443 | String tagValue = StringUtils.trim(phpDocTag.getTagValue());
444 | if (StringUtils.isNotBlank(tagValue)) {
445 | returns.add(tagValue);
446 | }
447 | }
448 | }
449 |
450 | // workaround for "@return T" is currently not WOrking
451 | for (PhpDocTag phpDocTag : phpDocComment.getTagElementsByName("@return")) {
452 | String text = StringUtils.trim(phpDocTag.getText().replaceAll("^\\s*@return\\s+", ""));
453 | if (StringUtils.isNotBlank(text)) {
454 | returns.add(text);
455 | }
456 | }
457 |
458 | return returns;
459 | }
460 |
461 | /**
462 | * Generate a full FQN class name out of a given short class name with respecting current namespace and use scope
463 | *
464 | * - "Foobar" needs to have its use statement attached
465 | * - No use statement match its on the same namespace as the class
466 | *
467 | * TODO: find a core function for this
468 | *
469 | * @param classNameScope \Foobar\Classes
470 | * @param shortClassName Foobar
471 | */
472 | public static String getFqnClassNameFromScope(@NotNull String classNameScope, @NotNull String shortClassName, @NotNull Map useImportMap) {
473 | // its already on the global namespace: "\Exception"
474 | if (shortClassName.startsWith("\\")) {
475 | return shortClassName;
476 | }
477 |
478 | // not use statement so stop here
479 | if (useImportMap.size() == 0) {
480 | return shortClassName;
481 | }
482 |
483 | // "Foo\Bar" split it on "subnamespace"; if no "subnamespace" only care about the first array item as out use match
484 | String[] split = shortClassName.split("\\\\");
485 | if (useImportMap.containsKey(split[0])) {
486 | String shortClassImport = useImportMap.get(split[0]);
487 |
488 | // on "Foo\Bar" we must extend also "Bar" for the import
489 | // "Foo\Bar" => "\Car\Foo\Bar"
490 | if (split.length > 1) {
491 | String[] yourArray = Arrays.copyOfRange(split, 1, split.length);
492 | shortClassImport += "\\" + StringUtils.join(yourArray, "\\");
493 | }
494 |
495 | return shortClassImport;
496 | }
497 |
498 | // strip the last namespace part and replace it with ours: "Foobar\Bar" => "Foobar\OurShortClass"
499 | return StringUtils.substringBeforeLast(classNameScope, "\\") + "\\" + shortClassName;
500 | }
501 | }
502 |
--------------------------------------------------------------------------------