├── .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 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 | 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 | [![Build Status](https://travis-ci.org/Haehnchen/idea-php-generics-plugin.svg?branch=master)](https://travis-ci.org/Haehnchen/idea-php-generics-plugin) 10 | [![Version](http://phpstorm.espend.de/badge/12754/version)](https://plugins.jetbrains.com/plugin/12754) 11 | [![Downloads](http://phpstorm.espend.de/badge/12754/downloads)](https://plugins.jetbrains.com/plugin/12754) 12 | [![Downloads last month](http://phpstorm.espend.de/badge/12754/last-month)](https://plugins.jetbrains.com/plugin/12754) 13 | [![Donate to this project using Paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](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 | ![class-string](https://plugins.jetbrains.com/files/12754/screenshot_20052.png) 150 | ![Object-like arrays](https://plugins.jetbrains.com/files/12754/screenshot_21124.png) 151 | ![Psalm Immutable](https://plugins.jetbrains.com/files/12754/screenshot_21166.png) 152 | ![Quality Tool](https://plugins.jetbrains.com/files/12754/screenshot_21656.png) 153 | ![Template Types](https://plugins.jetbrains.com/files/12754/screenshot_21688.png) 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 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 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 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 | --------------------------------------------------------------------------------