Repository: pestphp/pest-intellij
Branch: master
Commit: 8c7aca5430a1
Files: 461
Total size: 542.8 KB
Directory structure:
gitextract_h0dnztv0/
├── .editorconfig
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature-request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── auto-close.yml
│ ├── build.yml
│ ├── qodana.yml
│ ├── release.yml
│ └── run-ui-tests.yml
├── .gitignore
├── .run/
│ ├── Run IDE with Plugin.run.xml
│ ├── Run Plugin Tests.run.xml
│ ├── Run Plugin Verification.run.xml
│ └── Run Qodana.run.xml
├── BUILD.bazel
├── CHANGELOG.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── build.gradle.kts
├── coverage/
│ ├── BUILD.bazel
│ ├── intellij.pest.coverage.iml
│ ├── resources/
│ │ └── intellij.pest.coverage.xml
│ ├── src/
│ │ ├── PestCoverageEnabledConfiguration.kt
│ │ ├── PestCoverageEngine.kt
│ │ ├── PestCoverageProgramRunner.kt
│ │ └── features/
│ │ └── mutate/
│ │ ├── PestMutateProgramRunner.kt
│ │ └── PestMutateTestExecutor.kt
│ └── tests/
│ ├── BUILD.bazel
│ ├── intellij.pest.coverage.tests.iml
│ ├── src/
│ │ └── com/
│ │ └── intellij/
│ │ └── pest/
│ │ └── coverage/
│ │ ├── PestCoverageProgramRunnerTest.kt
│ │ └── features/
│ │ └── mutate/
│ │ └── PestMutateProgramRunnerTest.kt
│ └── testData/
│ ├── ATest.php
│ ├── features/
│ │ └── mutate/
│ │ ├── ATest.php
│ │ └── php
│ └── php
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── intellij.pest.iml
├── intellij.pest.tests.iml
├── plugin-content.yaml
├── settings.gradle.kts
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── pestphp/
│ │ └── pest/
│ │ ├── PestIcons.java
│ │ └── configuration/
│ │ ├── PestRunConfigurationSettings.java
│ │ └── PhpTestRunConfiguration.java
│ ├── kotlin/
│ │ └── com/
│ │ └── pestphp/
│ │ └── pest/
│ │ ├── FileUtil.kt
│ │ ├── PestBundle.kt
│ │ ├── PestComposerConfig.kt
│ │ ├── PestFrameworkType.kt
│ │ ├── PestFunctionsUtil.kt
│ │ ├── PestIconProvider.kt
│ │ ├── PestNamingUtil.kt
│ │ ├── PestNewTestFromClassAction.kt
│ │ ├── PestPluginDisposable.kt
│ │ ├── PestSettings.kt
│ │ ├── PestTestCreateInfo.kt
│ │ ├── PestTestDescriptor.kt
│ │ ├── PestTestFileUtil.kt
│ │ ├── PestTestRunLineMarkerProvider.kt
│ │ ├── PestUtil.kt
│ │ ├── annotator/
│ │ │ ├── PestAnnotator.kt
│ │ │ └── PestAnnotatorVisitor.kt
│ │ ├── completion/
│ │ │ ├── InternalMembersCompletionProvider.kt
│ │ │ ├── PestCompletionContributor.kt
│ │ │ ├── PestCustomExtensionCompletionProvider.kt
│ │ │ └── ThisFieldsCompletionProvider.kt
│ │ ├── configuration/
│ │ │ ├── PestDebugRunner.kt
│ │ │ ├── PestLocationProvider.kt
│ │ │ ├── PestRerunFailedTestsAction.kt
│ │ │ ├── PestRerunProfile.kt
│ │ │ ├── PestRunConfiguration.kt
│ │ │ ├── PestRunConfigurationHandler.kt
│ │ │ ├── PestRunConfigurationProducer.kt
│ │ │ ├── PestRunConfigurationType.kt
│ │ │ ├── PestRunnerSettings.kt
│ │ │ ├── PestTestRunConfigurationEditor.kt
│ │ │ └── PestVersionDetector.kt
│ │ ├── features/
│ │ │ ├── configuration/
│ │ │ │ ├── ConfigurationInDirectoryReferenceProvider.kt
│ │ │ │ ├── ConfigurationReferenceContributor.kt
│ │ │ │ └── PhpFolderReferenceSet.kt
│ │ │ ├── customExpectations/
│ │ │ │ ├── CustomExpectationIndex.kt
│ │ │ │ ├── CustomExpectationNotifier.kt
│ │ │ │ ├── CustomExpectationParameterInfoHandler.kt
│ │ │ │ ├── CustomExpectationRemoveGeneratedFileStartupActivity.kt
│ │ │ │ ├── ListMethodDataExternalizer.kt
│ │ │ │ ├── MethodDataExternalizer.kt
│ │ │ │ ├── expectationUtil.kt
│ │ │ │ ├── externalizers/
│ │ │ │ │ ├── ListDataExternalizer.kt
│ │ │ │ │ ├── MethodDataExternalizer.kt
│ │ │ │ │ ├── ParameterDataExternalizer.kt
│ │ │ │ │ └── PhpTypeDataExternalizer.kt
│ │ │ │ ├── generators/
│ │ │ │ │ ├── ExpectationGenerator.kt
│ │ │ │ │ ├── Method.kt
│ │ │ │ │ └── Parameter.kt
│ │ │ │ └── symbols/
│ │ │ │ ├── PestCustomExpectationReference.kt
│ │ │ │ ├── PestCustomExpectationReferenceProvider.kt
│ │ │ │ ├── PestCustomExpectationRenameUsageSearcher.kt
│ │ │ │ ├── PestCustomExpectationSymbol.kt
│ │ │ │ ├── PestCustomExpectationSymbolDeclaration.kt
│ │ │ │ ├── PestCustomExpectationSymbolDeclarationProvider.kt
│ │ │ │ └── PestCustomExpectationUsageSearcher.kt
│ │ │ ├── datasets/
│ │ │ │ ├── DatasetIndex.kt
│ │ │ │ ├── DatasetReference.kt
│ │ │ │ ├── DatasetReferenceContributor.kt
│ │ │ │ ├── DatasetReferenceProvider.kt
│ │ │ │ ├── DatasetUtil.kt
│ │ │ │ ├── InvalidDatasetNameCaseInspection.kt
│ │ │ │ └── InvalidDatasetReferenceInspection.kt
│ │ │ ├── parallel/
│ │ │ │ ├── PestParallelProgramRunner.kt
│ │ │ │ ├── PestParallelSMTEventsAdapter.kt
│ │ │ │ └── PestParallelTestExecutor.kt
│ │ │ └── snapshotTesting/
│ │ │ ├── SnapshotLineMarkerProvider.kt
│ │ │ └── SnapshotUtil.kt
│ │ ├── goto/
│ │ │ ├── PestDatasetUsagesGotoHandler.kt
│ │ │ ├── PestGotoTargetPresentationProvider.kt
│ │ │ ├── PestTestFinder.kt
│ │ │ └── PestTestGoToSymbolContributor.kt
│ │ ├── indexers/
│ │ │ └── PestTestIndex.kt
│ │ ├── inspections/
│ │ │ ├── ChangeMultipleExpectCallsToChainableQuickFix.kt
│ │ │ ├── ChangeTestNameCasingQuickFix.kt
│ │ │ ├── InvalidTestNameCaseInspection.kt
│ │ │ ├── MissingScreenshotSnapshotInspection.kt
│ │ │ ├── MultipleExpectChainableInspection.kt
│ │ │ ├── PestAssertionCanBeSimplifiedInspection.kt
│ │ │ ├── PestTestFailedLineInspection.kt
│ │ │ ├── SuppressExpressionResultUnusedInspection.kt
│ │ │ └── SuppressUndefinedPropertyInspection.kt
│ │ ├── notifications/
│ │ │ └── OutdatedNotification.kt
│ │ ├── parser/
│ │ │ ├── PestConfigurationFile.kt
│ │ │ └── PestConfigurationFileParser.kt
│ │ ├── runner/
│ │ │ ├── LocationInfo.kt
│ │ │ ├── PestConsoleProperties.kt
│ │ │ ├── PestFailedLineManager.kt
│ │ │ ├── PestPressToContinueAction.kt
│ │ │ ├── PestPromptConsoleFolding.kt
│ │ │ └── PestTestStackTraceParser.kt
│ │ ├── statistics/
│ │ │ └── PestUsagesCollector.kt
│ │ ├── structureView/
│ │ │ ├── PestStructureViewElement.kt
│ │ │ └── PestStructureViewExtension.kt
│ │ ├── surrounders/
│ │ │ ├── ExpectStatementSurrounder.kt
│ │ │ └── StatementSurroundDescriptor.kt
│ │ ├── templates/
│ │ │ ├── PestConfigNewDatasetFileAction.kt
│ │ │ ├── PestConfigNewFileAction.kt
│ │ │ ├── PestDescribePostfixTemplate.kt
│ │ │ ├── PestItPostfixTemplate.kt
│ │ │ ├── PestPostfixTemplateProvider.kt
│ │ │ └── PestRootTemplateContextType.kt
│ │ └── types/
│ │ ├── HigherOrderExtendTypeProvider.kt
│ │ ├── InnerTestTypeProvider.kt
│ │ ├── ThisExtendTypeProvider.kt
│ │ ├── ThisFieldTypeProvider.kt
│ │ └── ThisTypeProvider.kt
│ └── resources/
│ ├── META-INF/
│ │ └── plugin.xml
│ ├── fileTemplates/
│ │ └── internal/
│ │ ├── Pest It.php.ft
│ │ ├── Pest Scoped Dataset.php.ft
│ │ ├── Pest Shared Dataset.php.ft
│ │ ├── Pest Test.php.ft
│ │ └── Pest file from class.php.ft
│ ├── inspectionDescriptions/
│ │ ├── InvalidDatasetNameCaseInspection.html
│ │ ├── InvalidDatasetReferenceInspection.html
│ │ ├── InvalidTestNameCaseInspection.html
│ │ ├── MissingScreenshotSnapshotInspection.html
│ │ ├── MultipleExpectChainableInspection.html
│ │ ├── PestAssertionCanBeSimplifiedInspection.html
│ │ └── PestTestFailedLineInspection.html
│ ├── liveTemplates/
│ │ └── PestPHP.xml
│ ├── log4j.properties
│ ├── messages/
│ │ └── pestBundle.properties
│ └── postfixTemplates/
│ ├── PestDescribePostfixTemplate/
│ │ ├── after.php.template
│ │ ├── before.php.template
│ │ └── description.html
│ └── PestItPostfixTemplate/
│ ├── after.php.template
│ ├── before.php.template
│ └── description.html
└── test/
├── kotlin/
│ └── com/
│ └── pestphp/
│ └── pest/
│ ├── PestIconProviderTest.kt
│ ├── PestLightCodeFixture.kt
│ ├── PestTestRunLineMarkerProviderTest.kt
│ ├── annotator/
│ │ └── PestAnnotatorTest.kt
│ ├── codeInsight/
│ │ └── typeInference/
│ │ └── PestTypeInferenceTest.kt
│ ├── configuration/
│ │ ├── PestLocationProviderTest.kt
│ │ ├── PestRunConfigurationTest.kt
│ │ ├── PestVersionDetectorTest.kt
│ │ ├── PestVersionParserTest.kt
│ │ ├── pest/
│ │ │ └── PestConfigurationFileTest.kt
│ │ └── uses/
│ │ └── PestConfigurationFileTest.kt
│ ├── customExpectations/
│ │ ├── ListMethodDataExternalizerTest.kt
│ │ ├── MethodDataExternalizerTest.kt
│ │ └── generators/
│ │ └── ExpectationGeneratorTest.kt
│ ├── features/
│ │ ├── configuration/
│ │ │ ├── PestCompletionTest.kt
│ │ │ └── UsesCompletionTest.kt
│ │ ├── datasets/
│ │ │ ├── DatasetCompletionTest.kt
│ │ │ ├── DatasetGoToTest.kt
│ │ │ ├── DatasetIndexTest.kt
│ │ │ ├── DatasetReferenceTest.kt
│ │ │ ├── DatasetUsagesTest.kt
│ │ │ ├── InvalidDatasetNameCaseInspectionTest.kt
│ │ │ └── InvalidDatasetReferenceInspectionTest.kt
│ │ ├── parallel/
│ │ │ ├── PestParallelProgramRunnerTest.kt
│ │ │ └── PestParallelSMTEventsAdapterTest.kt
│ │ └── snapshotTesting/
│ │ ├── SnapshotLineMarkerProviderTest.kt
│ │ └── SnapshotUtilTest.kt
│ ├── generateTest/
│ │ └── PestNewTestFromClassActionTest.kt
│ ├── goto/
│ │ └── PestTestFinderTest.kt
│ ├── higherOrderExpectations/
│ │ ├── HigherOrderExpectationAssertionCompletionTest.kt
│ │ └── HigherOrderExpectationCompletionTest.kt
│ ├── indexers/
│ │ └── PestTestIndexTest.kt
│ ├── inspections/
│ │ ├── InvalidTestNameCaseInspectionTest.kt
│ │ ├── MissingScreenshotSnapshotInspectionTest.kt
│ │ ├── MultipleExpectChainableInspectionTest.kt
│ │ ├── PestAssertionCanBeSimplifiedInspectionTest.kt
│ │ ├── PestTestFailedLineInspectionTest.kt
│ │ └── PhpStormInspectionsTest.kt
│ ├── runner/
│ │ ├── PestPressToContinueActionTest.kt
│ │ └── PestTestStackTraceParserTest.kt
│ ├── surrounders/
│ │ ├── ExpectStatementSurrounderTest.kt
│ │ └── SurroundTestCase.kt
│ ├── templates/
│ │ └── PestPostfixTemplateProviderTest.kt
│ ├── types/
│ │ ├── BaseTypeTestCase.kt
│ │ ├── ExpectCallCompletionTest.kt
│ │ ├── FunctionTypeTest.kt
│ │ ├── ThisFieldCompletionTest.kt
│ │ ├── ThisFieldTypeTest.kt
│ │ └── ThisTypeTest.kt
│ └── utilTests/
│ ├── GetPestTestNameTests.kt
│ ├── GetPestTestsTest.kt
│ ├── IsPestTestFileTest.kt
│ ├── IsPestTestFunctionTest.kt
│ ├── PestUtilTest.kt
│ ├── ToPestFqnTests.kt
│ └── ToPestTestRegexTests.kt
└── resources/
└── com/
└── pestphp/
└── pest/
├── Dataset.php
├── Pest.php
├── PestTestRunLineMarkerProviderTest/
│ ├── AssignmentFunctionCallNamedTest.php
│ ├── AssignmentFunctionCallNamedTestWithoutPest.php
│ ├── FunctionCallNamedTestAsArgument.php
│ ├── FunctionCallNamedTestInsideDescribeBlock.php
│ ├── FunctionCallNamedTestInsideTest.php
│ ├── FunctionCallNamedTestWithoutPest.php
│ ├── MethodCallNamedItAndVariableTest.php
│ ├── NamedDataSets.php
│ ├── PestItFunctionCallWithDescriptionAndClosure.php
│ ├── PestItFunctionCallWithRedefinition.php
│ └── contextProject/
│ └── tests/
│ └── Test.php
├── PestUtil/
│ ├── Login.integration.test.php
│ ├── Login.test.php
│ ├── MethodCallNamedIt.php
│ ├── MethodCallNamedItAndVariableTest.php
│ ├── MethodCallNamedTest.php
│ ├── NestedDescribeFunctionCalls.php
│ ├── PestArchFunctionCall.php
│ ├── PestDescribeBlock.php
│ ├── PestDescribeBlockAndTestFunctionEndOfLine.php
│ ├── PestItFunctionCallWithConcatString.php
│ ├── PestItFunctionCallWithDescriptionAndClosure.php
│ ├── PestItFunctionCallWithDescriptionAndHigherOrder.php
│ ├── PestTestFunctionCallWithCircumflex.php
│ ├── PestTestFunctionCallWithConcatString.php
│ ├── PestTestFunctionCallWithDescriptionAndClosure.php
│ ├── PestTestFunctionCallWithDescriptionAndHigherOrder.php
│ ├── PestTestFunctionCallWithNamesapce.php
│ ├── PestTestFunctionCallWithParenthesis.php
│ ├── PestTestWithPlusAndQuestionMark.php
│ ├── User.spec.php
│ └── dir.name/
│ └── Test.php
├── SimpleHigherOrderNotTest.php
├── SimpleHigherOrderTestWithName.php
├── SimpleScript.php
├── TestWithDataset.php
├── annotator/
│ ├── DuplicateCustomExpectation.afterDelete.php
│ ├── DuplicateCustomExpectation.afterNavigate.php
│ ├── DuplicateCustomExpectation.php
│ ├── DuplicateTestName.afterDelete.php
│ ├── DuplicateTestName.afterNavigate.php
│ ├── DuplicateTestName.php
│ ├── DuplicateTestNameInDescribeBlock.php
│ ├── NoDuplicateCustomExpectation.php
│ ├── NoDuplicateTestName.php
│ └── stub/
│ └── Functions.php
├── codeInsight/
│ └── typeInference/
│ ├── ThisInInnerClosure.php
│ └── ThisInSubproject/
│ └── Test.php
├── configuration/
│ ├── FileWithPestTest.php
│ ├── locationProvider/
│ │ ├── DescribeBlock/
│ │ │ └── subdir/
│ │ │ └── Test.php
│ │ ├── DescribeBlockIt/
│ │ │ └── subdir/
│ │ │ └── Test.php
│ │ ├── SubprojectFor1xVersion/
│ │ │ └── subdir/
│ │ │ └── Test.php
│ │ └── SubprojectFor2xVersion/
│ │ └── subdir/
│ │ └── Test.php
│ ├── pest/
│ │ ├── tests/
│ │ │ ├── DIRFeature/
│ │ │ │ └── FeatureTest.php
│ │ │ ├── DynamicFeature/
│ │ │ │ └── FeatureTest.php
│ │ │ ├── Feature/
│ │ │ │ └── FeatureTest.php
│ │ │ ├── GlobPattern/
│ │ │ │ ├── DirectoryTest.php
│ │ │ │ ├── FileTest.php
│ │ │ │ └── FileWithRelativePathTest.php
│ │ │ ├── GroupedFeature/
│ │ │ │ └── GroupedFeatureTest.php
│ │ │ ├── Pest.php
│ │ │ └── Unit/
│ │ │ ├── PestExtendUnitTest.php
│ │ │ └── UnitTest.php
│ │ └── tests2/
│ │ └── Unit/
│ │ └── UnitTest.php
│ ├── php
│ └── uses/
│ ├── DIRFeature/
│ │ └── FeatureTest.php
│ ├── DynamicFeature/
│ │ └── FeatureTest.php
│ ├── Feature/
│ │ └── FeatureTest.php
│ ├── GlobPattern/
│ │ ├── DirectoryTest.php
│ │ ├── FileTest.php
│ │ └── FileWithRelativePathTest.php
│ ├── GroupedFeature/
│ │ └── GroupedFeatureTest.php
│ ├── Pest.php
│ └── Unit/
│ ├── UnitTest.php
│ └── UsesUnitTest.php
├── customExpectations/
│ ├── CustomExpectation.php
│ ├── CustomExpectationWithParameter.php
│ ├── CustomThisExpectation.php
│ ├── CustomUserExpectation.php
│ ├── UnfinishedCustomExpectation.php
│ ├── generators/
│ │ └── ExpectationGenerator/
│ │ └── GeneratedWithMethod.php
│ └── subFolder/
│ └── CustomExpectation.php
├── features/
│ ├── configuration/
│ │ ├── pest/
│ │ │ ├── CompleteFakePestInFolder.php
│ │ │ ├── CompleteInFolder.php
│ │ │ └── Test.php
│ │ └── uses/
│ │ ├── CompleteFakeInFolder.php
│ │ ├── CompleteInFolder.php
│ │ └── Test.php
│ ├── datasets/
│ │ ├── AutocompleteDatasetTest.php
│ │ ├── DatasetInDescribeBlock.php
│ │ ├── DatasetInDescribeBlockCompletion.php
│ │ ├── DatasetInDescribeBlockReference.php
│ │ ├── DatasetInsideDescribeBlockTest.php
│ │ ├── DatasetNoArgsTest.php
│ │ ├── DatasetOnNonPestTest.php
│ │ ├── DatasetOnNonPestTestCompletion.php
│ │ ├── DatasetReference.php
│ │ ├── DatasetTest.php
│ │ ├── Datasets.php
│ │ ├── DoubleWithDatasetReference.php
│ │ ├── InvalidDatasetInDescribeBlockTest.php
│ │ ├── InvalidDatasetNameCase.after.php
│ │ ├── InvalidDatasetNameCase.php
│ │ ├── InvalidDatasetTest.php
│ │ ├── NotDatasetReference.php
│ │ ├── SharedDatasetReference.php
│ │ └── ValidDatasetNameCase.php
│ └── parallel/
│ ├── ATest.php
│ └── php
├── generateTest/
│ ├── testWithNamespace.after.php
│ └── testWithNamespace.php
├── goto/
│ ├── PestTestFinder/
│ │ ├── App/
│ │ │ └── User.php
│ │ └── test/
│ │ └── App/
│ │ ├── MockTest.php
│ │ ├── UserDescribeTest.php
│ │ └── UserTest.php
│ └── datasetUsages/
│ ├── DatasetDeclaration.php
│ └── DatasetUsage.php
├── higherOrderExpectations/
│ ├── .phpstorm.meta.php
│ ├── ExpectMethodAssertionCompletion.php
│ ├── ExpectMethodAssertionCompletionChained.php
│ ├── ExpectMethodAssertionCompletionChainedAssertions.php
│ ├── ExpectMethodCompletion.php
│ ├── ExpectMethodCompletionChained.php
│ ├── ExpectMethodCompletionChainedAssertions.php
│ ├── ExpectPropertyAssertionCompletion.php
│ ├── ExpectPropertyAssertionCompletionChained.php
│ ├── ExpectPropertyAssertionCompletionChainedAssertions.php
│ ├── ExpectPropertyCompletion.php
│ ├── ExpectPropertyCompletionChained.php
│ ├── ExpectPropertyCompletionChainedAssertions.php
│ └── stubs.php
├── indexers/
│ └── PestTestIndexTest/
│ ├── FileWithDescribeBlockTest.php
│ ├── FileWithPestTest.php
│ ├── FileWithPestTodosTest.php
│ └── FileWithoutPestTest.php
├── inspections/
│ ├── ExpectCallsWithOtherStatementsBetween.php
│ ├── InvalidTestNameAndDatasetName.after.php
│ ├── InvalidTestNameAndDatasetName.php
│ ├── InvalidTestNameCase.after.php
│ ├── InvalidTestNameCase.php
│ ├── ManyExpectCall.after.php
│ ├── ManyExpectCall.php
│ ├── MultipleExpectCall.after.php
│ ├── MultipleExpectCall.php
│ ├── MultipleExpectCallsWithOtherStatementsBetween.after.php
│ ├── MultipleExpectCallsWithOtherStatementsBetween.php
│ ├── SingleExpectCall.php
│ ├── ValidHigherOrderTestNameCase.php
│ ├── ValidItTestNameWithoutSpaces.php
│ ├── ValidTestNameCase.php
│ ├── ValidTestNameWhenWrongCasingOnOneWord.php
│ ├── assertionCanBeSimplified/
│ │ ├── ToBeWithFalse.after.php
│ │ ├── ToBeWithFalse.php
│ │ ├── ToBeWithNull.after.php
│ │ ├── ToBeWithNull.php
│ │ ├── ToBeWithTrue.after.php
│ │ ├── ToBeWithTrue.php
│ │ ├── ToHaveCountWithZero.after.php
│ │ └── ToHaveCountWithZero.php
│ ├── pestTestFailedLine/
│ │ ├── AnonymousFunction.php
│ │ ├── FailedOneLine.php
│ │ ├── LambdaFunction.php
│ │ ├── MismatchLine.php
│ │ ├── MultipleStatementsInOneLine.php
│ │ ├── OutRange.php
│ │ ├── SingleLeafElementReported.php
│ │ ├── TypeBefore.php
│ │ ├── TypeInside.php
│ │ ├── WithDataSet.php
│ │ ├── WithDataSetAndKeys.php
│ │ ├── WithDataSetAndSeveralFails.php
│ │ └── WithNamedDataSet.php
│ ├── phpstorm/
│ │ └── MultipleClassesDeclarationsInPestFileTest.php
│ └── screenshotProject/
│ └── tests/
│ ├── .pest/
│ │ └── snapshots/
│ │ └── Feature/
│ │ ├── ScreenshotSnapshot/
│ │ │ └── it_browser_testing.snap
│ │ ├── ScreenshotSnapshotComplexName/
│ │ │ └── it_1__2_3_4_.snap
│ │ ├── ScreenshotSnapshotMultiple/
│ │ │ ├── it_test.snap
│ │ │ └── it_test2.snap
│ │ └── nested/
│ │ └── ScreenshotSnapshotNested/
│ │ └── nested.snap
│ └── Feature/
│ ├── MissingScreenshotSnapshot.php
│ ├── MissingScreenshotSnapshotComplexName.php
│ ├── MissingScreenshotSnapshotMultiple.php
│ ├── ScreenshotSnapshot.php
│ ├── ScreenshotSnapshotComplexName.php
│ ├── ScreenshotSnapshotMultiple.php
│ └── nested/
│ ├── MissingScreenshotSnapshotNested.php
│ └── ScreenshotSnapshotNested.php
├── runner/
│ └── pestTestStacktraceParser/
│ ├── Multiline.php
│ ├── OneLine.php
│ ├── OneLineRemote.php
│ ├── OutRangeLineNumber.php
│ └── WrongLineNumber.php
├── snapshotTesting/
│ ├── allSnapshotAssertions.php
│ ├── nonSnapshotAssertions.php
│ ├── snapshotAssertionUseStatement.php
│ ├── snapshotTest.php
│ └── tests/
│ └── __snapshots__/
│ └── snapshotTest__it_renders_correctly__1.txt
├── stubs.php
├── templates/
│ ├── describe.after.php
│ ├── describe.php
│ ├── it.after.php
│ └── it.php
├── types/
│ ├── TestCase.php
│ ├── expect/
│ │ ├── expectCallCompletion.php
│ │ ├── expectCallCompletionChainedNotMethod.php
│ │ ├── expectCallCompletionChainedNotProperty.php
│ │ ├── expectExtendCallOnNonExpectFunction.php
│ │ ├── expectExtendReturnType.php
│ │ ├── expectInvalidExtendNoReturnType.php
│ │ └── extendCallOnChainedExpectation.php
│ ├── function/
│ │ └── testTest.php
│ ├── this/
│ │ ├── beforeEach.php
│ │ ├── itShortLambdaTest.php
│ │ ├── itTest.php
│ │ └── testTest.php
│ └── thisField/
│ ├── afterEach.php
│ ├── afterEachNamespace.php
│ ├── beforeEach.php
│ ├── beforeEachCompletion.php
│ ├── beforeEachNamespace.php
│ └── beforeEachNamespaceCompletion.php
└── utilTests/
├── ClassNameResolutionInNamespaceTest.php
├── ClassNameResolutionTest.php
└── SimpleTest.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
[*]
indent_size = 4
[{*.kt,*.kts}]
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_size = 4
================================================
FILE: .github/FUNDING.yml
================================================
github: olivernybroe
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
Please report a bug to [YouTrack](https://youtrack.jetbrains.com/newIssue?project=WI)
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
Create a discussion instead.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
- [ ] Added or updated tests
- [ ] Updated `CHANGELOG.md`
issues: #...
================================================
FILE: .github/dependabot.yml
================================================
# Dependabot configuration:
# https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "daily"
================================================
FILE: .github/workflows/auto-close.yml
================================================
on:
issues:
types: [opened, edited]
jobs:
auto_close_issues:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Automatically close issues that don't follow the issue template
uses: lucasbento/auto-close-issues@v1.0.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-close-message: "@${issue.user.login}: hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template." # optional property
================================================
FILE: .github/workflows/build.yml
================================================
# GitHub Actions Workflow created for testing and preparing the plugin release in following steps:
# - validate Gradle Wrapper,
# - run 'test' and 'verifyPlugin' tasks,
# - run Qodana inspections,
# - run 'buildPlugin' task and prepare artifact for the further tests,
# - run 'runPluginVerifier' task,
# - create a draft release.
#
# Workflow is triggered on push and pull_request events.
#
# GitHub Actions reference: https://help.github.com/en/actions
#
## JBIJPPTPL
name: Build
on:
# Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests)
push:
branches: [main]
# Trigger the workflow on any pull request
pull_request:
jobs:
# Run Gradle Wrapper Validation Action to verify the wrapper's checksum
# Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks
# Build plugin and provide the artifact for the next workflow jobs
build:
name: Build
runs-on: ubuntu-latest
outputs:
version: ${{ steps.properties.outputs.version }}
changelog: ${{ steps.properties.outputs.changelog }}
steps:
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2.4.0
# Validate wrapper
- name: Gradle Wrapper Validation
uses: gradle/wrapper-validation-action@v1.0.4
# Setup Java 11 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: zulu
java-version: 11
cache: gradle
# Set environment variables
- name: Export Properties
id: properties
shell: bash
run: |
PROPERTIES="$(./gradlew properties --console=plain -q)"
VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')"
NAME="$(echo "$PROPERTIES" | grep "^pluginName:" | cut -f2- -d ' ')"
CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)"
CHANGELOG="${CHANGELOG//'%'/'%25'}"
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
echo "::set-output name=version::$VERSION"
echo "::set-output name=name::$NAME"
echo "::set-output name=changelog::$CHANGELOG"
echo "::set-output name=pluginVerifierHomeDir::~/.pluginVerifier"
./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier
# Run tests
- name: Run Tests
run: ./gradlew test
# Collect Tests Result of failed tests
- name: Collect Tests Result
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: tests-result
path: ${{ github.workspace }}/build/reports/tests
# # Cache Plugin Verifier IDEs
# - name: Setup Plugin Verifier IDEs Cache
# uses: actions/cache@v2.1.7
# with:
# path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides
# key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }}
#
# # Run Verify Plugin task and IntelliJ Plugin Verifier tool
# - name: Run Plugin Verification tasks
# run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }}
#
# # Collect Plugin Verifier Result
# - name: Collect Plugin Verifier Result
# if: ${{ always() }}
# uses: actions/upload-artifact@v2
# with:
# name: pluginVerifier-result
# path: ${{ github.workspace }}/build/reports/pluginVerifier
# TODO: temp needed because verifier disabled
- run: ./gradlew buildPlugin
# Prepare plugin archive content for creating artifact
- name: Prepare Plugin Artifact
id: artifact
shell: bash
run: |
cd ${{ github.workspace }}/build/distributions
FILENAME=`ls *.zip`
unzip "$FILENAME" -d content
echo "::set-output name=filename::${FILENAME:0:-4}"
# Store already-built plugin as an artifact for downloading
- name: Upload artifact
uses: actions/upload-artifact@v2.2.4
with:
name: ${{ steps.artifact.outputs.filename }}
path: ./build/distributions/content/*/*
# Prepare a draft release for GitHub Releases page for the manual verification
# If accepted and published, release workflow would be triggered
releaseDraft:
name: Release Draft
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
steps:
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2.4.0
# Remove old release drafts by using the curl request for the available releases with draft flag
- name: Remove Old Release Drafts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api repos/{owner}/{repo}/releases \
--jq '.[] | select(.draft == true) | .id' \
| xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{}
# Create new release draft - which is not publicly visible and requires manual acceptance
- name: Create Release Draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create v${{ needs.build.outputs.version }} \
--draft \
--title "v${{ needs.build.outputs.version }}" \
--notes "$(cat << 'EOM'
${{ needs.build.outputs.changelog }}
EOM
)"
================================================
FILE: .github/workflows/qodana.yml
================================================
name: Qodana
on:
workflow_dispatch:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
qodana:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Setup Java 11 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: zulu
java-version: 11
cache: gradle
# Build
- name: Run Build
run: ./gradlew build
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2023.1.0
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
================================================
FILE: .github/workflows/release.yml
================================================
# GitHub Actions Workflow created for handling the release process based on the draft release prepared
# with the Build workflow. Running the publishPlugin task requires the PUBLISH_TOKEN secret provided.
name: Release
on:
release:
types: [prereleased, released]
jobs:
# Prepare and publish the plugin to the Marketplace repository
release:
name: Publish Plugin
runs-on: ubuntu-latest
steps:
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2.4.0
with:
ref: ${{ github.event.release.tag_name }}
# Setup Java 11 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: zulu
java-version: 11
cache: gradle
# Set environment variables
- name: Export Properties
id: properties
shell: bash
run: |
CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d'
${{ github.event.release.body }}
EOM
)"
CHANGELOG="${CHANGELOG//'%'/'%25'}"
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
echo "::set-output name=changelog::$CHANGELOG"
# Update Unreleased section with the current release note
- name: Patch Changelog
if: ${{ steps.properties.outputs.changelog != '' }}
env:
CHANGELOG: ${{ steps.properties.outputs.changelog }}
run: |
./gradlew patchChangelog --release-note="$CHANGELOG"
# Publish the plugin to the Marketplace
- name: Publish Plugin
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
run: ./gradlew publishPlugin
# Upload artifact as a release asset
- name: Upload Release Asset
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/*
# Create pull request
- name: Create Pull Request
if: ${{ steps.properties.outputs.changelog != '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ github.event.release.tag_name }}"
BRANCH="changelog-update-$VERSION"
git config user.email "action@github.com"
git config user.name "GitHub Action"
git checkout -b $BRANCH
git commit -am "Changelog update - $VERSION"
git push --set-upstream origin $BRANCH
gh pr create \
--title "Changelog update - \`$VERSION\`" \
--body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \
--base main \
--head $BRANCH
================================================
FILE: .github/workflows/run-ui-tests.yml
================================================
# GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps:
# - prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with UI
# - wait for IDE to start
# - run UI tests with separate Gradle task
#
# Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform
#
# Workflow is triggered manually.
name: Run UI Tests
on:
workflow_dispatch
jobs:
testUI:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
runIde: |
export DISPLAY=:99.0
Xvfb -ac :99 -screen 0 1920x1080x16 &
gradle runIdeForUiTests &
- os: windows-latest
runIde: start gradlew.bat runIdeForUiTests
- os: macos-latest
runIde: ./gradlew runIdeForUiTests &
steps:
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2.4.0
# Setup Java 11 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: zulu
java-version: 11
cache: gradle
# Run IDEA prepared for UI testing
- name: Run IDE
run: ${{ matrix.runIde }}
# Wait for IDEA to be started
- name: Health Check
uses: jtalk/url-health-check-action@v2
with:
url: http://127.0.0.1:8082
max-attempts: 15
retry-delay: 30s
# Run tests
- name: Tests
run: ./gradlew test
================================================
FILE: .gitignore
================================================
.gradle
build
.idea
.intellijPlatform
================================================
FILE: .run/Run IDE with Plugin.run.xml
================================================
true
true
false
================================================
FILE: .run/Run Plugin Tests.run.xml
================================================
true
true
false
================================================
FILE: .run/Run Plugin Verification.run.xml
================================================
true
true
false
================================================
FILE: .run/Run Qodana.run.xml
================================================
true
true
false
================================================
FILE: BUILD.bazel
================================================
### auto-generated section `build intellij.pest` start
load("@rules_jvm//:jvm.bzl", "jvm_library")
jvm_library(
name = "pest",
module_name = "intellij.pest",
visibility = ["//visibility:public"],
srcs = glob(["src/main/java/**/*.kt", "src/main/java/**/*.java", "src/main/java/**/*.form", "src/main/kotlin/**/*.kt", "src/main/kotlin/**/*.java", "src/main/kotlin/**/*.form"], allow_empty = True),
resources = glob(["src/main/resources/**/*"]),
resource_strip_prefix = "src/main/resources",
deps = [
"@community//platform/core-api:core",
"@community//platform/execution",
"@community//platform/execution-impl",
"@community//platform/ide-core-impl",
"@community//platform/platform-impl:ide-impl",
"//phpstorm/php-openapi:php",
"@community//platform/analysis-api:analysis",
"@community//platform/lang-core",
"@community//platform/projectModel-api:projectModel",
"@community//platform/remote-core",
"@community//platform/structure-view-impl:structureView-impl",
"@community//platform/util",
"@community//platform/indexing-api:indexing",
"//phpstorm/php:php-impl",
"@community//platform/analysis-impl",
"@community//platform/editor-ui-api:editor-ui",
"@community//platform/smRunner",
"@community//platform/platform-util-io:ide-util-io",
"@community//platform/ide-core",
"@community//platform/lang-api:lang",
"@community//platform/lang-impl",
"@community//xml/xml-psi-impl:psi-impl",
"@community//libraries/fastutil",
"@community//xml/dom-impl",
"@community//platform/core-ui",
"@community//platform/core-impl",
"@community//platform/util/text-matching",
"@community//platform/util:util-ui",
"@community//platform/statistics",
"@community//platform/testRunner",
]
)
jvm_library(
name = "pest_test_lib",
testonly = True,
module_name = "intellij.pest",
visibility = ["//visibility:public"],
srcs = glob([], allow_empty = True),
runtime_deps = [
":pest",
"@community//platform/core-api:core_test_lib",
"@community//platform/execution:execution_test_lib",
"@community//platform/execution-impl:execution-impl_test_lib",
"@community//platform/ide-core-impl:ide-core-impl_test_lib",
"@community//platform/platform-impl:ide-impl_test_lib",
"//phpstorm/php-openapi:php_test_lib",
"@community//platform/analysis-api:analysis_test_lib",
"@community//jps/model-api:model",
"@community//jps/model-api:model_test_lib",
"@community//platform/lang-core:lang-core_test_lib",
"@community//platform/projectModel-api:projectModel_test_lib",
"@community//platform/remote-core:remote-core_test_lib",
"@community//platform/structure-view-impl:structureView-impl_test_lib",
"@community//platform/testFramework",
"@community//platform/testFramework:testFramework_test_lib",
"@community//platform/util:util_test_lib",
"@community//platform/indexing-api:indexing_test_lib",
"//phpstorm/php:php-impl_test_lib",
"@community//platform/analysis-impl:analysis-impl_test_lib",
"@community//platform/editor-ui-api:editor-ui_test_lib",
"@community//platform/smRunner:smRunner_test_lib",
"@community//platform/platform-util-io:ide-util-io_test_lib",
"@community//platform/ide-core:ide-core_test_lib",
"@community//platform/lang-api:lang_test_lib",
"@community//platform/lang-impl:lang-impl_test_lib",
"@community//xml/xml-psi-impl:psi-impl_test_lib",
"@community//libraries/fastutil:fastutil_test_lib",
"@community//xml/dom-impl:dom-impl_test_lib",
"@community//platform/core-ui:core-ui_test_lib",
"@community//platform/core-impl:core-impl_test_lib",
"@community//platform/util/text-matching:text-matching_test_lib",
"@community//platform/util:util-ui_test_lib",
"@lib//:io-mockk",
"@lib//:io-mockk-jvm",
"@community//platform/statistics:statistics_test_lib",
"@community//platform/testRunner:testRunner_test_lib",
]
)
### auto-generated section `build intellij.pest` end
### auto-generated section `iml intellij.pest` start
exports_files([
"intellij.pest.iml",
], visibility = ["//visibility:public"])
### auto-generated section `iml intellij.pest` end
### auto-generated section `build intellij.pest.tests` start
jvm_library(
name = "pest-tests",
module_name = "intellij.pest.tests",
visibility = ["//visibility:public"],
srcs = glob([], allow_empty = True)
)
jvm_library(
name = "pest-tests_test_lib",
testonly = True,
visibility = ["//visibility:public"],
srcs = glob(["src/test/kotlin/**/*.kt", "src/test/kotlin/**/*.java", "src/test/kotlin/**/*.form"], allow_empty = True),
resources = glob(["src/test/resources/**/*"]),
resource_strip_prefix = "src/test/resources",
associates = [
"//phpstorm/pest",
"//phpstorm/pest:pest_test_lib",
],
deps = [
"@community//platform/core-api:core",
"@community//platform/core-api:core_test_lib",
"@community//platform/execution",
"@community//platform/execution:execution_test_lib",
"@community//platform/execution-impl",
"@community//platform/execution-impl:execution-impl_test_lib",
"@community//platform/ide-core-impl",
"@community//platform/platform-impl:ide-impl",
"//phpstorm/php-openapi:php",
"//phpstorm/php-openapi:php_test_lib",
"@community//platform/analysis-api:analysis",
"@community//platform/analysis-api:analysis_test_lib",
"@community//jps/model-api:model",
"@community//jps/model-api:model_test_lib",
"@community//platform/lang-core",
"@community//platform/lang-core:lang-core_test_lib",
"@community//platform/projectModel-api:projectModel",
"@community//platform/projectModel-api:projectModel_test_lib",
"@community//platform/remote-core",
"@community//platform/remote-core:remote-core_test_lib",
"@community//platform/structure-view-impl:structureView-impl",
"@community//platform/structure-view-impl:structureView-impl_test_lib",
"@community//platform/testFramework",
"@community//platform/testFramework:testFramework_test_lib",
"@community//platform/util",
"@community//platform/util:util_test_lib",
"@community//platform/indexing-api:indexing",
"@community//platform/indexing-api:indexing_test_lib",
"//phpstorm/php:php-impl",
"//phpstorm/php:php-impl_test_lib",
"@community//platform/analysis-impl",
"@community//platform/analysis-impl:analysis-impl_test_lib",
"@community//platform/editor-ui-api:editor-ui",
"@community//platform/editor-ui-api:editor-ui_test_lib",
"@community//platform/smRunner",
"@community//platform/smRunner:smRunner_test_lib",
"@community//platform/platform-util-io:ide-util-io",
"@community//platform/ide-core",
"@community//platform/lang-api:lang",
"@community//platform/lang-api:lang_test_lib",
"@community//platform/lang-impl",
"@community//platform/lang-impl:lang-impl_test_lib",
"@community//xml/xml-psi-impl:psi-impl",
"@community//xml/xml-psi-impl:psi-impl_test_lib",
"@community//libraries/fastutil",
"@community//libraries/fastutil:fastutil_test_lib",
"@community//plugins/coverage-common:coverage",
"@community//plugins/coverage-common:coverage_test_lib",
"@community//xml/dom-impl",
"@community//xml/dom-impl:dom-impl_test_lib",
"@community//platform/core-ui",
"@community//platform/core-ui:core-ui_test_lib",
"@community//platform/core-impl",
"@community//platform/core-impl:core-impl_test_lib",
"@community//platform/util/text-matching",
"@community//platform/util/text-matching:text-matching_test_lib",
"@community//platform/util:util-ui",
"@community//platform/util:util-ui_test_lib",
"@lib//:io-mockk",
"@lib//:io-mockk-jvm",
"@community//platform/statistics",
"@community//platform/statistics:statistics_test_lib",
"@community//platform/testRunner",
"@community//platform/testRunner:testRunner_test_lib",
],
runtime_deps = [
":pest-tests",
"@community//platform/ide-core-impl:ide-core-impl_test_lib",
"@community//platform/platform-impl:ide-impl_test_lib",
"@community//platform/platform-util-io:ide-util-io_test_lib",
"@community//platform/ide-core:ide-core_test_lib",
]
)
### auto-generated section `build intellij.pest.tests` end
### auto-generated section `iml intellij.pest.tests` start
exports_files([
"intellij.pest.tests.iml",
], visibility = ["//visibility:public"])
### auto-generated section `iml intellij.pest.tests` end
### auto-generated section `test intellij.pest.tests` start
load("@community//build:tests-options.bzl", "jps_test")
jps_test(
name = "pest-tests_test",
runtime_deps = [":pest-tests_test_lib"]
)
### auto-generated section `test intellij.pest.tests` end
================================================
FILE: CHANGELOG.md
================================================
# PEST IntelliJ Changelog
## Unreleased
### Added
- Added proper resolve for custom expectations
- Added proper rename for custom expectations
- Added migration startup activity to delete redundant generated `Expectation.php` file
### Fixed
- Fixed infinite "Closing project..." dialog issue on project close
### Changed
- Reworked custom expectations engine using Symbol API
- Removed `Expectation.php` generation
## 1.11.0 - 2023-09-12
### Added
- Added support for running tests in describe block ([#498](https://github.com/pestphp/pest-intellij/pull/498))
### Fixed
- Fixed property declared dynamically showing warning in pest test cases
- Fixed goto and rerun tests not working on new pest versions
## 1.10.1 - 2023-05-31
### Changed
- Changed pest file creation to two actions (tests and dataset)
### Added
- Save test flavour preferences when creating a new test
## 1.10.0 - 2023-05-31
### Added
- Added pest file creation support
### Fixed
- Remove test sources filter lookup, as it breaks others plugins
## 1.9.3 - 2023-05-31
### Fixed
- Fixed file icon missing if all tests has property calls
- Fixed gutter icon not updating state correctly
- Fixed test names with `[` and `]` not being matched correctly
- Fixed test name casing inspection not working correctly with `it` tests
## 1.9.2 - 2023-03-01
### Fixed
- Fixed "Preferred Coverage Engine" not being saved
## 1.9.1 - 2023-02-28
### Fixed
- Fixed ComposerLibraryManager being nullable now.
- Fixed running tests with filenames containing `_`.
### Changed
- Changed logic for base path to be from composer.json file.
## 1.9.0 - 2023-01-15
### Added
- Added support for running specific tests on Pest 2.0
- Added support for running todo's as tests
### Fixed
- Fixed running tests with `?` in the name
## 1.8.3
### Added
- Added support for test names with string concat statements
- Added stacktrace folding for Pest 2.0 output
### Changed
- Removed the "test started at" text on the test console output
### Fixed
- Fixed regex to match tests that have both named and unnamed datasets
## 1.8.1
### Fixed
- Fixed originalFile in iconProvider sometimes being null
- Fixed DuplicateCustomExpectation testing crashing on unfinished inspections
## 1.8.0
### Added
- Added support for using goto location when using remote interpreters
### Fixed
- Fixed nested `readAction` calls in Icon Provider
### Changed
- Changed Icon Provider to use indexes for better performance
## 1.7.0
### Added
- Added `uses->in` folder reference
- Added registry entry for disabling expectation file generation
### Changed
- Changed goto and completion contributor to reference provider
- Changed icons to use build-in dark mode switching
### Fixed
- Fixed dataset reference error when no dataset provided yet.
## 1.6.2
### Fixed
- Fixed duplicate type provider key with nette plugin
## 1.6.1
### Added
- Added inspection for checking if dataset exists
### Fixed
- Fixed dataset autocompletion triggering on all strings
- Fixed dataset goto triggering on all strings
## 1.6.0
### Added
- Added converting multiple `expect` to `and` calls instead
- Added dataset completion
- Added dataset goto
### Fixed
- Fixed automatic case changing on multicased string
## 1.5.0
### Added
- Added automatic case changing to pest test names
## 1.4.2
### Fixed
- Changed runReadAction to runReadActionInSmartMode in startup activity
## 1.4.1
### Changed
- Reduced custom expectation index size by over 95%
### Fixed
- Check if file exist in index (can happen if file is deleted outside IDE)
- Handle path separators per OS
## 1.4.0
### Added
- Added support for dynamic paths in `uses->in` statements
- Added inspection for duplicate custom expectation name
- Add surrounder for `expect`
### Changed
- Define root path from phpunit.xml instead of composer path
### Fixed
- Remove `-` from the pest generated regex
- Escape `/` in regex method name
## 1.3.0
### Fixed
- Changed services to light services for auto disposable
- Fixed null pointer error when no virtual file
### Changed
- Change reporting on GitHub to contain full stacktracepa
### Added
- Added higher order expectation type provider
- Added support for xdebug3 and xdebug2 coverage option
## 1.2.2
### Fixed
- Hide snapshot icon for import statements
- Fix ArrayIndex error from ExpectationFileService
- Fixed wrong file expectation matching in ExpectationFileService
### Added
- Add support for in calls
- Added support for running key value datasets
### Changed
- Changed root path for regex to be based of vendor dir location instead of working directory
### Removed
- Remove service message newline requirement as method is deprecated
## 1.2.1
### Fixed
- Moved file generation into smart invocation
## 1.2.0
### Added
- Added gutter icon for snapshots
- Added goto snapshot file
### Fixed
- Rewrote the custom expectation system to use a more robust system
- Updated custom expectation indexer to v2
### Changed
- Removed decorator in favor of implementing interface
## 1.1.0
### Fixed
- Invoke the FileListener PSI part later (should fix indexing issues)
- Fixed stub issues on PestIconProvider by wrapping `runReadAction`
- Fixed `$this->field` not working when namespace exist
- Fixed Concurrent modification errors on expectation file service
- Fixed file generation triggering on projects without pest
### Added
- Added new context type for the root of a pest file
- Added post fix template for `it` tests
- Added live template for `it` test
- Added live template for `test` test
- Added light icon for `pest.php` file
## 1.0.5
### Changed
- Bumped min IntelliJ version to 2021.1
## 1.0.4
### Added
- Added Suppress inspection for `$this->field`
## 1.0.3
### Fixed
- Fixed php type resolving during event dispatching on file listener
- Fixed PSI and index mismatch on file listener
## 1.0.2
### Fixed
- Fixed indexes being out of date in file listener
## 1.0.1
### Fixed
- Removed usage of globalType (needed for 2020.3 support)
## 1.0.0
### Added
- Added structure support for tests
- Added autocompletion for custom expectations
- Added pest icon for the Pest.php config file
- Added symbol contributor for pest tests
### Fixed
- Fixed a read only permission bug when used with Code with me
- Fixed wrong namespace in custom expectations file generation
## 0.4.3
### Added
- Added IntelliJ version to bug report
- Added new Dataset icons (Thanks @caneco!)
- Added test state icons
- Added run all test in file icon
### Fixed
- Fix support for 2021.1
- Fix running tests with circumflex (^)
### Changed
- Bumped min IntelliJ version to 2020.3
## 0.4.2
### Added
- Added path mapping support ([#77](https://github.com/pestphp/pest-intellij/pull/77))
### Changed
- Bumped min plugin version to 2020.2
- Bumped Java version to 11
### Removed
- Disabled version checking (did not work with path mapping) ([#77](https://github.com/pestphp/pest-intellij/pull/77))
### Fixed
- Escape parenthesis in regex for single test ([#80](https://github.com/pestphp/pest-intellij/pull/80))
- Suppressed expression result unused inspection when on Pest test function element. ([#84](https://github.com/pestphp/pest-intellij/pull/84))
## 0.4.1
### Added
- Added support for auto-generated `it` test names. ([#72](https://github.com/pestphp/pest-intellij/pull/72))
### Changed
- Made the regex tightly bound and reused the same regex in rerun action. ([#72](https://github.com/pestphp/pest-intellij/pull/72))
## 0.4.0
### Added
- Added support for showing pest version ([#52](https://github.com/pestphp/pest-intellij/pull/52))
- Type provider for Pest test functions ([#48](https://github.com/pestphp/pest-intellij/pull/48))
- Added support for navigation between tests and test subject ([#53](https://github.com/pestphp/pest-intellij/pull/53))
- Added error reporting to GitHub issues ([#55](https://github.com/pestphp/pest-intellij/pull/55))
- Added inspection for duplicate test names in same file. ([#56](https://github.com/pestphp/pest-intellij/pull/56))
- Added completions for static and protected $this methods. ([#66](https://github.com/pestphp/pest-intellij/pull/66))
- Added completions $this fields declared in beforeEach functions. ([#66](https://github.com/pestphp/pest-intellij/pull/66))
- Added pcov coverage engine support ([#64](https://github.com/pestphp/pest-intellij/pull/64))
### Fixed
- Fixed duplicate test name error when no test name is given yet. ([#61](https://github.com/pestphp/pest-intellij/pull/61))
- Fixed finding tests with namespace at the top. ([#65](https://github.com/pestphp/pest-intellij/pull/65))
- Fixed allow location to be null in location provider. ([#68](https://github.com/pestphp/pest-intellij/pull/68))
- Fixed rerunning tests with namespaces ([#69](https://github.com/pestphp/pest-intellij/pull/69))
## 0.3.3
### Fixed
- Fixed running with dataset ([#47](https://github.com/pestphp/pest-intellij/pull/47))
## 0.3.2
### Added
- Added dark/light mode icons ([#45](https://github.com/pestphp/pest-intellij/pull/45))
## 0.3.1
### Changed
- Change the name of the plugin
## 0.3.0
### Added
- Basic autocompletion for `$this` for PhpUnit TestCase base class ([#11](https://github.com/pestphp/pest-intellij/pull/11))
- Line markers now works, for the whole file and the single test. ([#17](https://github.com/pestphp/pest-intellij/pull/17), [#24](https://github.com/pestphp/pest-intellij/pull/24))
- Add running support with debugger ([#19](https://github.com/pestphp/pest-intellij/pull/19))
- `$this->field` type support for fields declared in `beforeEach` and `beforeAll` functions ([#22](https://github.com/pestphp/pest-intellij/pull/22))
- Run tests scoped based on the cursor ([#24](https://github.com/pestphp/pest-intellij/pull/24), [#26](https://github.com/pestphp/pest-intellij/pull/26))
- Added support for rerun tests when using new pest version ([#39](https://github.com/pestphp/pest-intellij/pull/39))
- Added coverage support with clover output ([#39](https://github.com/pestphp/pest-intellij/pull/39))
### Changed
- Migrated all Java classes to Kotlin
### Fixed
- Plugin require restart as PhpTestFrameworkType does not support dynamic plugins
- Line markers reported false positives with method calls([#31](https://github.com/pestphp/pest-intellij/pull/31))
## 0.1.1
### Added
- Initial scaffold created from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template)
================================================
FILE: CLAUDE.md
================================================
# Pest Plugin
## Mock Usage Guidelines
Prefer MockK to other mocking approaches.
When using MockK, prefer explicit stubbing over inline lambda syntax:
```kotlin
val config = mockk()
every { config.project } returns project
every { config.name } returns "Test"
```
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Pest IntelliJ
Thank you for wanting to contribute!
## What should I know before getting started?
The project is coded in Kotlin using the IntelliJ platform.
The IntelliJ platform has a great wiki for documentation which is recommended to get familiar for understanding
many of the things happening in this project.
[plugins.jetbrains.com/docs/intellij](https://plugins.jetbrains.com/docs/intellij/welcome.html)
## How to run project locally?
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) Oliver Nybroe olivernybroe@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Pest IntelliJ
This plugin adds support for using Pest PHP inside PHPStorm
## Installation
- Using IDE built-in plugin system:
Preferences > Plugins > Marketplace > Search for "Pest" >
Install Plugin
- Manually:
Download the [latest release](https://github.com/pestphp/pest-intellij/releases/latest) and install it manually using
Preferences > Plugins > ⚙️ > Install plugin from disk...
- Using Early Access Program (EAP) builds:
Preferences > Plugins > ⚙️ > Manage plugin repositories
Add a new entry for [`https://plugins.jetbrains.com/plugins/eap/14636`](https://plugins.jetbrains.com/plugins/eap/14636)
Then search for the plugin and install it as usual.
## Configuration
To configure pest to run properly, you need to setup the the proper local test framework
- Navigate to
Preferences > Languages & Frameworks > PHP > Test Frameworks
And add the following two configuration fields:
Set "Path to Pest Executable" to
/path/to/your/project/vendor/bin/pest
Set the "Test Runner" to
/path/to/your/project/phpunit.xml
## Resources
For a great video course on how to write tests with Pest, check out [Testing Laravel](https://testing-laravel.com/) or [Pest From Scratch](https://laracasts.com/series/pest-from-scratch).
## Issue?
Please report it to [YouTrack](https://youtrack.jetbrains.com/newIssue?project=WI)
## Credits
Originally developed by [Oliver Nybroe](https://github.com/olivernybroe)
---
Plugin based on the [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template).
================================================
FILE: build.gradle.kts
================================================
import org.jetbrains.intellij.platform.gradle.ProductMode
import org.jetbrains.changelog.Changelog
import org.jetbrains.changelog.markdownToHTML
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
fun properties(key: String) = project.findProperty(key).toString()
plugins {
// Java support
id("java")
// Kotlin support
id("org.jetbrains.kotlin.jvm") version "2.3.0"
// Gradle IntelliJ Plugin
id("org.jetbrains.intellij.platform") version "2.7.0"
// Gradle Changelog Plugin
id("org.jetbrains.changelog") version "2.2.0"
}
group = properties("pluginGroup")
version = properties("pluginVersion")
// Configure project's dependencies
repositories {
mavenCentral()
intellijPlatform {
defaultRepositories()
}
}
dependencies {
implementation(kotlin("stdlib"))
testImplementation("io.mockk:mockk:1.14.3") {
exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-core-jvm")
}
testImplementation("junit:junit:4.13.2")
intellijPlatform {
val type = providers.gradleProperty("platformType")
val version = providers.gradleProperty("platformVersion")
create(type, version) {
useInstaller = false
productMode = ProductMode.MONOLITH
}
testFramework(TestFrameworkType.Platform)
bundledPlugins(properties("platformBundledPlugins").toPlugins())
bundledModules(properties("platformBundledModules").toPlugins())
}
}
// Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin
changelog {
version.set(properties("pluginVersion"))
groups.set(emptyList())
}
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(providers.gradleProperty("javaVersion").get()))
}
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(providers.gradleProperty("javaVersion").get()))
}
}
tasks {
withType().configureEach {
val javaVersion = properties("javaVersion")
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
wrapper {
gradleVersion = properties("gradleVersion")
}
patchPluginXml {
version = properties("pluginVersion")
sinceBuild.set(properties("pluginSinceBuild"))
untilBuild.set(properties("pluginUntilBuild"))
// Extract the section from README.md and provide for the plugin's manifest
pluginDescription.set(
projectDir.resolve("README.md").readText().lines().run {
val start = ""
val end = ""
if (!containsAll(listOf(start, end))) {
throw GradleException("Plugin description section not found in README.md:\n$start ... $end")
}
subList(indexOf(start) + 1, indexOf(end))
}.joinToString("\n").run { markdownToHTML(this) }
)
// Get the latest available change notes from the changelog file
changeNotes.set(provider {
changelog.renderItem(changelog.run {
getOrNull(properties("pluginVersion")) ?: getLatest()
}, Changelog.OutputType.HTML)
})
}
signPlugin {
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
privateKey.set(System.getenv("PRIVATE_KEY"))
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
}
publishPlugin {
dependsOn("patchChangelog")
token.set(System.getenv("PUBLISH_TOKEN"))
// pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3
// Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more:
// https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel
// channels = listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first())
}
}
private fun String.toPlugins(): List = split(',')
.map(String::trim)
.filter(String::isNotEmpty)
================================================
FILE: coverage/BUILD.bazel
================================================
### auto-generated section `build intellij.pest.coverage` start
load("@rules_jvm//:jvm.bzl", "jvm_library")
jvm_library(
name = "coverage",
module_name = "intellij.pest.coverage",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
resources = glob(["resources/**/*"]),
resource_strip_prefix = "resources",
deps = [
"@lib//:kotlin-stdlib",
"//phpstorm/pest",
"//phpstorm/php:php-impl",
"@community//plugins/coverage-common:coverage",
"@community//platform/execution",
"@community//platform/smRunner",
"@community//platform/core-api:core",
"@community//platform/util",
"@community//platform/util:util-ui",
"@community//platform/ide-core",
"@community//platform/statistics",
"@community//platform/testRunner",
"//phpstorm/coverage",
]
)
jvm_library(
name = "coverage_test_lib",
testonly = True,
module_name = "intellij.pest.coverage",
visibility = ["//visibility:public"],
srcs = glob([], allow_empty = True),
runtime_deps = [
":coverage",
"//phpstorm/pest:pest_test_lib",
"//phpstorm/php:php-impl_test_lib",
"@community//plugins/coverage-common:coverage_test_lib",
"@community//platform/execution:execution_test_lib",
"@community//platform/smRunner:smRunner_test_lib",
"@community//platform/core-api:core_test_lib",
"@community//platform/util:util_test_lib",
"@community//platform/util:util-ui_test_lib",
"@community//platform/ide-core:ide-core_test_lib",
"@community//platform/statistics:statistics_test_lib",
"@community//platform/lang-api:lang",
"@community//platform/lang-api:lang_test_lib",
"@community//platform/projectModel-impl",
"@community//platform/projectModel-impl:projectModel-impl_test_lib",
"@community//platform/projectModel-api:projectModel",
"@community//platform/projectModel-api:projectModel_test_lib",
"@community//platform/platform-util-io:ide-util-io",
"@community//platform/platform-util-io:ide-util-io_test_lib",
"@community//platform/testRunner:testRunner_test_lib",
"//phpstorm/coverage:coverage_test_lib",
]
)
### auto-generated section `build intellij.pest.coverage` end
### auto-generated section `iml intellij.pest.coverage` start
exports_files([
"intellij.pest.coverage.iml",
], visibility = ["//visibility:public"])
### auto-generated section `iml intellij.pest.coverage` end
================================================
FILE: coverage/intellij.pest.coverage.iml
================================================
================================================
FILE: coverage/resources/intellij.pest.coverage.xml
================================================
================================================
FILE: coverage/src/PestCoverageEnabledConfiguration.kt
================================================
package com.intellij.pest.coverage
import com.intellij.coverage.CoverageRunner
import com.intellij.execution.configurations.coverage.CoverageEnabledConfiguration
import com.intellij.php.coverage.PhpUnitCoverageRunner
import com.pestphp.pest.configuration.PestRunConfiguration
class PestCoverageEnabledConfiguration(
configuration: PestRunConfiguration
) : CoverageEnabledConfiguration(configuration, CoverageRunner.getInstance(PhpUnitCoverageRunner::class.java)) {
override fun coverageFileNameSeparator(): String = "@"
}
================================================
FILE: coverage/src/PestCoverageEngine.kt
================================================
package com.intellij.pest.coverage
import com.intellij.coverage.CoverageFileProvider
import com.intellij.coverage.CoverageRunner
import com.intellij.coverage.CoverageSuite
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.configurations.coverage.CoverageEnabledConfiguration
import com.intellij.openapi.project.Project
import com.intellij.php.coverage.PhpCoverageSuite
import com.intellij.php.coverage.PhpUnitCoverageEngine
import com.pestphp.pest.configuration.PestRunConfiguration
class PestCoverageEngine : PhpUnitCoverageEngine() {
override fun isApplicableTo(conf: RunConfigurationBase<*>): Boolean {
return conf is PestRunConfiguration
}
override fun createCoverageEnabledConfiguration(conf: RunConfigurationBase<*>): CoverageEnabledConfiguration {
return PestCoverageEnabledConfiguration(conf as PestRunConfiguration)
}
@Deprecated("Deprecated in Java")
override fun createCoverageSuite(
covRunner: CoverageRunner,
name: String,
coverageDataFileProvider: CoverageFileProvider,
config: CoverageEnabledConfiguration
): CoverageSuite? {
if (config is PestCoverageEnabledConfiguration) {
return PhpCoverageSuite(name, config.configuration.project, covRunner, coverageDataFileProvider, config.createTimestamp())
}
return null
}
override fun createCoverageSuite(
name: String,
project: Project,
covRunner: CoverageRunner,
coverageDataFileProvider: CoverageFileProvider,
timestamp: Long,
config: CoverageEnabledConfiguration
): CoverageSuite? {
if (config is PestCoverageEnabledConfiguration) {
return PhpCoverageSuite(name, project, covRunner, coverageDataFileProvider, timestamp)
}
return null
}
}
================================================
FILE: coverage/src/PestCoverageProgramRunner.kt
================================================
package com.intellij.pest.coverage
import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.php.coverage.PhpCoverageRunner
import com.jetbrains.php.config.commandLine.PhpCommandSettings
import com.jetbrains.php.config.commandLine.PhpCommandSettingsBuilder
import com.jetbrains.php.config.interpreters.PhpInterpreter
import com.jetbrains.php.debug.xdebug.options.XdebugConfigurationOptionsManager
import com.jetbrains.php.phpunit.coverage.PhpUnitCoverageEngine.CoverageEngine
import com.jetbrains.php.run.PhpConfigurationOption
import com.jetbrains.php.run.PhpRunConfigurationHolder
import com.pestphp.pest.configuration.PestRunConfiguration
import com.pestphp.pest.features.parallel.addParallelArguments
open class PestCoverageProgramRunner : PhpCoverageRunner() {
companion object {
const val EXECUTOR_ID: String = "Coverage"
const val RUNNER_ID: String = "PestCoverageRunner"
}
override fun canRun(executorId: String, profile: RunProfile): Boolean {
return executorId == EXECUTOR_ID && profile is PestRunConfiguration
}
override fun createCoverageArguments(targetCoverage: String?): MutableList {
val coverageArguments: ArrayList = ArrayList()
coverageArguments.add("--coverage-clover")
targetCoverage?.let { coverageArguments.add(it) }
return coverageArguments
}
override fun getRunnerId(): String = RUNNER_ID
override fun createState(
env: ExecutionEnvironment,
interpreter: PhpInterpreter,
runConfigurationHolder: PhpRunConfigurationHolder<*>,
coverageArguments: MutableList,
localCoverage: String,
targetCoverage: String
): RunProfileState? {
val runConfiguration = runConfigurationHolder.runConfiguration as PestRunConfiguration
val command = createPestCoverageCommand(runConfiguration, interpreter, coverageArguments, localCoverage, targetCoverage)
return runConfiguration.checkAndGetState(env, command)
}
fun createPestCoverageCommand(
runConfiguration: PestRunConfiguration,
interpreter: PhpInterpreter,
coverageArguments: List,
localCoverage: String,
targetCoverage: String
): PhpCommandSettings {
val command = PhpCommandSettingsBuilder(runConfiguration.project, interpreter)
.loadDebugExtension().build().apply {
runConfiguration.applyTestArguments(this, coverageArguments)
}
val options = when (runConfiguration.pestSettings.pestRunnerSettings.coverageEngine) {
CoverageEngine.XDEBUG -> XdebugConfigurationOptionsManager
.getConfigurationOptionsProvider(runConfiguration.project, interpreter)
.enableCoverage()
.createXdebugConfigurations()
CoverageEngine.PCOV -> listOf(PhpConfigurationOption("pcov.enabled", 1))
else -> throw IllegalArgumentException("Unsupported coverage engine.")
}
command.addConfigurationOptions(options)
addParallelArguments(runConfiguration, command)
setAdditionalMapping(localCoverage, targetCoverage, command)
return command
}
}
================================================
FILE: coverage/src/features/mutate/PestMutateProgramRunner.kt
================================================
package com.intellij.pest.coverage.features.mutate
import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.pest.coverage.PestCoverageProgramRunner
import com.pestphp.pest.PestBundle
import com.pestphp.pest.configuration.PestRunConfiguration
import com.pestphp.pest.features.parallel.postprocessExecutionResult
import com.pestphp.pest.statistics.PestUsagesCollector
private val MUTATE_ARGUMENTS: MutableList = mutableListOf("--mutate")
open class PestMutateProgramRunner : PestCoverageProgramRunner() {
companion object {
const val RUNNER_ID: String = "PestMutateRunner"
}
override fun doExecute(state: RunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? {
PestUsagesCollector.logMutationTestExecution(environment.project)
val contentDescriptor = super.doExecute(state, environment)
if (contentDescriptor != null) {
postprocessExecutionResult(contentDescriptor, environment, PestBundle.message("MUTATION_TESTING_IS_AVAILABLE_FROM_VERSION_3"))
}
return contentDescriptor
}
override fun canRun(executorId: String, profile: RunProfile): Boolean =
executorId == PestMutateTestExecutor.EXECUTOR_ID && profile is PestRunConfiguration
override fun createCoverageArguments(targetCoverage: String?): MutableList = MUTATE_ARGUMENTS
override fun getRunnerId(): String = RUNNER_ID
}
================================================
FILE: coverage/src/features/mutate/PestMutateTestExecutor.kt
================================================
package com.intellij.pest.coverage.features.mutate
import com.intellij.execution.Executor
import com.intellij.icons.AllIcons
import com.intellij.openapi.util.IconLoader.getDisabledIcon
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.util.text.TextWithMnemonic
import com.intellij.openapi.wm.ToolWindowId
import com.pestphp.pest.PestBundle
import com.pestphp.pest.PestIcons
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import javax.swing.Icon
internal class PestMutateTestExecutor : Executor() {
companion object {
const val EXECUTOR_ID: @NonNls String = "PestMutateTestExecutor"
const val CONTEXT_ACTION_ID: @NonNls String = "PestRunMutate"
}
override fun getToolWindowId(): String = ToolWindowId.RUN
override fun getToolWindowIcon(): Icon = AllIcons.Toolwindows.ToolWindowRun
override fun getIcon(): Icon = PestIcons.RunWithMutate
override fun getRerunIcon(): Icon = AllIcons.Actions.Rerun
override fun getDisabledIcon(): Icon = getDisabledIcon(icon)
override fun getDescription(): String = PestBundle.message("ACTION_RUN_SELECTED_CONFIGURATION_WITH_MUTATE_DESCRIPTION")
override fun getActionName(): String = PestBundle.message("ACTION_PEST_MUTATE_TEXT")
override fun getId(): String = EXECUTOR_ID
override fun getStartActionText(): @Nls(capitalization = Nls.Capitalization.Title) String = PestBundle.message("RUN_PEST_WITH_MUTATE")
override fun getStartActionText(configurationName: String): String {
val configName = if (StringUtil.isEmpty(configurationName)) "" else " '${shortenNameIfNeeded(configurationName)}'"
return TextWithMnemonic.parse(PestBundle.message("RUN_S_WITH_MUTATE")).replaceFirst("%s", configName).toString()
}
override fun getContextActionId(): String = CONTEXT_ACTION_ID
override fun getHelpId(): String? = null
}
================================================
FILE: coverage/tests/BUILD.bazel
================================================
### auto-generated section `build intellij.pest.coverage.tests` start
load("@rules_jvm//:jvm.bzl", "jvm_library")
jvm_library(
name = "tests",
module_name = "intellij.pest.coverage.tests",
visibility = ["//visibility:public"],
srcs = glob([], allow_empty = True),
runtime_deps = ["@community//platform/ide-core"]
)
jvm_library(
name = "tests_test_lib",
testonly = True,
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
resources = glob(["testData/**/*"]),
resource_strip_prefix = "testData",
associates = [
"//phpstorm/pest/coverage",
"//phpstorm/pest/coverage:coverage_test_lib",
],
deps = [
"//phpstorm/pest:pest-tests",
"//phpstorm/pest:pest-tests_test_lib",
"@community//platform/lang-api:lang",
"@community//platform/lang-api:lang_test_lib",
"//phpstorm/php:php-impl",
"//phpstorm/php:php-impl_test_lib",
"//phpstorm/pest",
"//phpstorm/pest:pest_test_lib",
"@community//platform/core-api:core",
"@community//platform/core-api:core_test_lib",
"@community//platform/execution",
"@community//platform/execution:execution_test_lib",
"@community//platform/projectModel-api:projectModel",
"@community//platform/projectModel-api:projectModel_test_lib",
"@community//platform/projectModel-impl",
"@community//platform/projectModel-impl:projectModel-impl_test_lib",
"@community//platform/platform-util-io:ide-util-io",
"@community//platform/ide-core",
"@community//platform/smRunner",
"@community//platform/smRunner:smRunner_test_lib",
"@community//plugins/coverage-common:coverage",
"@community//plugins/coverage-common:coverage_test_lib",
"@community//platform/testRunner",
"@community//platform/testRunner:testRunner_test_lib",
"//phpstorm/coverage",
"//phpstorm/coverage:coverage_test_lib",
],
runtime_deps = [
":tests",
"@community//platform/platform-util-io:ide-util-io_test_lib",
"@community//platform/ide-core:ide-core_test_lib",
]
)
### auto-generated section `build intellij.pest.coverage.tests` end
### auto-generated section `iml intellij.pest.coverage.tests` start
exports_files([
"intellij.pest.coverage.tests.iml",
], visibility = ["//visibility:public"])
### auto-generated section `iml intellij.pest.coverage.tests` end
### auto-generated section `test intellij.pest.coverage.tests` start
load("@community//build:tests-options.bzl", "jps_test")
jps_test(
name = "tests_test",
runtime_deps = [":tests_test_lib"]
)
### auto-generated section `test intellij.pest.coverage.tests` end
================================================
FILE: coverage/tests/intellij.pest.coverage.tests.iml
================================================
================================================
FILE: coverage/tests/src/com/intellij/pest/coverage/PestCoverageProgramRunnerTest.kt
================================================
package com.intellij.pest.coverage
import com.intellij.coverage.CoverageDataManager
import com.intellij.coverage.CoverageHelper
import com.intellij.execution.PsiLocation
import com.intellij.execution.actions.ConfigurationContext
import com.intellij.psi.PsiElement
import com.intellij.testFramework.TestDataPath
import com.intellij.testFramework.fixtures.IdeaTestExecutionPolicy
import com.jetbrains.php.config.interpreters.PhpInterpreter
import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl
import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration
import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager
import com.pestphp.pest.PestFrameworkType
import com.pestphp.pest.PestLightCodeFixture
import com.pestphp.pest.configuration.PestRunConfiguration
import com.pestphp.pest.configuration.PestRunConfigurationProducer
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.pathString
@TestDataPath($$"$CONTENT_ROOT/testData")
class PestCoverageProgramRunnerTest : PestLightCodeFixture() {
private lateinit var configurationsBackup: List
override fun getTestDataPath(): String {
val intellijPath = Path.of(IdeaTestExecutionPolicy.getHomePathWithPolicy(), "phpstorm/pest/coverage/tests/testData")
return if (intellijPath.exists()) {
intellijPath.pathString
} else {
"testData"
}
}
fun testCannotRunWrongExecutorId() = doTest {
val configuration = createConfiguration(myFixture.file)
assertFalse(PestCoverageProgramRunner().canRun(PestCoverageProgramRunner.EXECUTOR_ID + "1", configuration))
}
fun testCanRunFile() = doTest {
val configuration = createConfiguration(myFixture.file)
assertTrue(PestCoverageProgramRunner().canRun(PestCoverageProgramRunner.EXECUTOR_ID, configuration))
}
fun testCanRunFunction() = doTest {
val testElement = myFixture.file?.firstChild?.lastChild?.firstChild ?: return@doTest
val configuration = createConfiguration(testElement)
assertTrue(PestCoverageProgramRunner().canRun(PestCoverageProgramRunner.EXECUTOR_ID, configuration))
}
fun testCanRunDirectory() = doTest {
val testElement = myFixture.file?.containingDirectory ?: return@doTest
val configuration = createConfiguration(testElement)
assertTrue(PestCoverageProgramRunner().canRun(PestCoverageProgramRunner.EXECUTOR_ID, configuration))
}
fun testBuildFile() = doTest {
val configuration = createConfiguration(myFixture.file)
val pestCoverageCommandSettings = PestCoverageProgramRunner().createPestCoverageCommand(configuration, configuration.interpreter!!, emptyList(), "", "")
val expected = "-dxdebug.coverage_enable=1 -dxdebug.mode=coverage randomPath --teamcity /src/ATest.php"
assertEquals(expected, pestCoverageCommandSettings.createGeneralCommandLine().parametersList.list.joinToString(" "))
}
fun testBuildFunction() = doTest {
val testElement = myFixture.file?.firstChild?.lastChild?.firstChild ?: return@doTest
val configuration = createConfiguration(testElement)
val pestCoverageCommandSettings = PestCoverageProgramRunner().createPestCoverageCommand(configuration, configuration.interpreter!!, emptyList(), "", "")
val expected = "-dxdebug.coverage_enable=1 -dxdebug.mode=coverage randomPath --teamcity /src/ATest.php"
assertEquals(
expected,
pestCoverageCommandSettings.createGeneralCommandLine().parametersList.list
.joinToString(" ")
.substringBefore(" --filter")
)
}
fun testBuildDirectory() = doTest {
val testElement = myFixture.file?.containingDirectory ?: return@doTest
val configuration = createConfiguration(testElement)
val pestCoverageCommandSettings = PestCoverageProgramRunner().createPestCoverageCommand(configuration, configuration.interpreter!!, emptyList(), "", "")
val expected = "-dxdebug.coverage_enable=1 -dxdebug.mode=coverage randomPath --teamcity /src"
assertEquals(expected, pestCoverageCommandSettings.createGeneralCommandLine().parametersList.list.joinToString(" "))
}
fun testBuildFileWithEnabledParallelTesting() = doTest {
val configuration = createConfiguration(myFixture.file)
configuration.pestSettings.pestRunnerSettings.parallelTestingEnabled = true
val pestCoverageCommandSettings = PestCoverageProgramRunner().createPestCoverageCommand(configuration, configuration.interpreter!!, emptyList(), "", "")
val expected = "-dxdebug.coverage_enable=1 -dxdebug.mode=coverage randomPath --teamcity /src/ATest.php --parallel --log-teamcity php://stdout"
assertEquals(expected, pestCoverageCommandSettings.createGeneralCommandLine().parametersList.list.joinToString(" "))
}
fun testCreateCoverageSuiteOnRunningCoverageTests() = doTest {
val configuration = createConfiguration(myFixture.file)
CoverageHelper.resetCoverageSuit(configuration)
assertSize(1, CoverageDataManager.getInstance(project).getSuites())
}
private fun createConfiguration(psiElement: PsiElement): PestRunConfiguration {
createPestFrameworkConfiguration()
val context = ConfigurationContext.createEmptyContextForLocation(PsiLocation.fromPsiElement(psiElement))
val runConfiguration = PestRunConfigurationProducer().createConfigurationFromContext(context)?.configuration as? PestRunConfiguration
runConfiguration!!.settings.commandLineSettings.interpreterSettings.interpreterName = getTestName(false)
return runConfiguration
}
private fun doTest(block: () -> Unit) {
myFixture.configureByFile("ATest.php")
block()
}
override fun setUp() {
super.setUp()
val interpreter = PhpInterpreter().apply {
name = getTestName(false)
homePath = "$testDataPath/php"
}
PhpInterpretersManagerImpl.getInstance(project).addInterpreter(interpreter)
configurationsBackup = PhpTestFrameworkSettingsManager.getInstance(project).getConfigurations(PestFrameworkType.Companion.instance)
}
override fun tearDown() {
try {
PhpTestFrameworkSettingsManager.getInstance(project).setConfigurations(PestFrameworkType.Companion.instance, configurationsBackup)
PhpInterpretersManagerImpl.getInstance(project).interpreters = emptyList()
} catch (e: Throwable) {
addSuppressedException(e)
} finally {
super.tearDown()
}
}
}
================================================
FILE: coverage/tests/src/com/intellij/pest/coverage/features/mutate/PestMutateProgramRunnerTest.kt
================================================
package com.intellij.pest.coverage.features.mutate
import com.intellij.execution.PsiLocation
import com.intellij.execution.actions.ConfigurationContext
import com.intellij.psi.PsiElement
import com.intellij.testFramework.TestDataPath
import com.intellij.testFramework.fixtures.IdeaTestExecutionPolicy
import com.jetbrains.php.config.interpreters.PhpInterpreter
import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl
import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration
import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager
import com.pestphp.pest.PestFrameworkType
import com.pestphp.pest.PestLightCodeFixture
import com.pestphp.pest.configuration.PestRunConfiguration
import com.pestphp.pest.configuration.PestRunConfigurationProducer
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.pathString
@TestDataPath($$"$CONTENT_ROOT/testData/features/mutate")
class PestMutateProgramRunnerTest : PestLightCodeFixture() {
private lateinit var configurationsBackup: List
override fun getTestDataPath(): String {
val intellijPath = Path.of(IdeaTestExecutionPolicy.getHomePathWithPolicy(), "phpstorm/pest/coverage/tests/testData/features/mutate")
return if (intellijPath.exists()) {
intellijPath.pathString
} else {
"testData/features/mutate"
}
}
fun testCannotRunWrongExecutorId() = doTest {
val configuration = createConfiguration(myFixture.file)
assertFalse(PestMutateProgramRunner().canRun(PestMutateTestExecutor.EXECUTOR_ID + "1", configuration))
}
fun testCanRunFile() = doTest {
val configuration = createConfiguration(myFixture.file)
assertTrue(PestMutateProgramRunner().canRun(PestMutateTestExecutor.EXECUTOR_ID, configuration))
}
fun testCanRunFunction() = doTest {
val testElement = myFixture.file?.firstChild?.lastChild?.firstChild ?: return@doTest
val configuration = createConfiguration(testElement)
assertTrue(PestMutateProgramRunner().canRun(PestMutateTestExecutor.EXECUTOR_ID, configuration))
}
fun testCanRunDirectory() = doTest {
val testElement = myFixture.file?.containingDirectory ?: return@doTest
val configuration = createConfiguration(testElement)
assertTrue(PestMutateProgramRunner().canRun(PestMutateTestExecutor.EXECUTOR_ID, configuration))
}
private fun createConfiguration(psiElement: PsiElement): PestRunConfiguration {
createPestFrameworkConfiguration()
val context = ConfigurationContext.createEmptyContextForLocation(PsiLocation.fromPsiElement(psiElement))
val runConfiguration = PestRunConfigurationProducer().createConfigurationFromContext(context)?.configuration
as? PestRunConfiguration
runConfiguration!!.settings.commandLineSettings.interpreterSettings.interpreterName = getTestName(false)
return runConfiguration
}
private fun doTest(block: () -> Unit) {
myFixture.configureByFile("ATest.php")
block()
}
override fun setUp() {
super.setUp()
val interpreter = PhpInterpreter().apply {
name = getTestName(false)
homePath = "$testDataPath/php"
}
PhpInterpretersManagerImpl.getInstance(project).addInterpreter(interpreter)
configurationsBackup = PhpTestFrameworkSettingsManager.getInstance(project).getConfigurations(PestFrameworkType.instance)
}
override fun tearDown() {
try {
PhpTestFrameworkSettingsManager.getInstance(project).setConfigurations(PestFrameworkType.instance, configurationsBackup)
PhpInterpretersManagerImpl.getInstance(project).interpreters = emptyList()
} catch (e: Throwable) {
addSuppressedException(e)
} finally {
super.tearDown()
}
}
}
================================================
FILE: coverage/tests/testData/ATest.php
================================================
https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html
pluginGroup = com.pestphp
pluginName = PEST PHP
pluginVersion = 1.12.0
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild = 232.10072.32
pluginUntilBuild =
platformType = PS
platformVersion = LATEST-EAP-SNAPSHOT
platformDownloadSources = true
# https://plugins.jetbrains.com/plugin/6610-php/versions
platformBundledPlugins = com.jetbrains.php
platformBundledModules = intellij.platform.coverage
# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3
javaVersion = 21
gradleVersion = 8.13
# Opt-out flag for bundling Kotlin standard library.
# See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details.
# suppress inspection "UnusedProperty"
kotlin.stdlib.default.dependency = false
org.gradle.jvmargs=-Xmx2048m
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: intellij.pest.iml
================================================
================================================
FILE: intellij.pest.tests.iml
================================================
================================================
FILE: plugin-content.yaml
================================================
- name: lib/modules/intellij.pest.coverage.jar
contentModules:
- name: intellij.pest.coverage
- name: lib/pest.jar
modules:
- name: intellij.pest
================================================
FILE: settings.gradle.kts
================================================
rootProject.name = "pest-intellij"
pluginManagement {
repositories {
maven {
url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
}
gradlePluginPortal()
}
}
================================================
FILE: src/main/java/com/pestphp/pest/PestIcons.java
================================================
package com.pestphp.pest;
import com.intellij.ui.IconManager;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
/**
* NOTE THIS FILE IS AUTO-GENERATED
* DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead
*/
public final class PestIcons {
private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) {
return IconManager.getInstance().loadRasterizedIcon(path, PestIcons.class.getClassLoader(), cacheKey, flags);
}
/** 16x16 */ public static final @NotNull Icon Config = load("config.svg", 710701582, 2);
/** 16x16 */ public static final @NotNull Icon Dataset = load("dataset.svg", -1986461428, 2);
/** 16x16 */ public static final @NotNull Icon File = load("file.svg", -1158724446, 2);
/** 16x16 */ public static final @NotNull Icon Logo = load("logo.svg", -2116012898, 2);
public static final class METAINF {
/** 16x16 */ public static final @NotNull Icon PluginIcon = load("META-INF/pluginIcon.svg", 1914567053, 0);
}
/** 16x16 */ public static final @NotNull Icon Run = load("run.svg", -452832596, 2);
/** 16x16 */ public static final @NotNull Icon RunWithMutate = load("runWithMutate.svg", 226904416, 2);
}
================================================
FILE: src/main/java/com/pestphp/pest/configuration/PestRunConfigurationSettings.java
================================================
package com.pestphp.pest.configuration;
import com.intellij.util.xmlb.annotations.Property;
import com.intellij.util.xmlb.annotations.Transient;
import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationSettings;
import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings;
import org.jetbrains.annotations.NotNull;
public class PestRunConfigurationSettings extends PhpTestRunConfigurationSettings {
@Override
protected @NotNull PestRunnerSettings createDefault() {
return new PestRunnerSettings();
}
@Property(surroundWithTag = false)
public @NotNull PestRunnerSettings getPestRunnerSettings() {
final PhpTestRunnerSettings settings = super.getRunnerSettings();
if (settings instanceof PestRunnerSettings) {
return (PestRunnerSettings)settings;
}
final PestRunnerSettings copy = PestRunnerSettings.fromPhpTestRunnerSettings(settings);
setPestRunnerSettings(copy);
return copy;
}
public void setPestRunnerSettings(PestRunnerSettings runnerSettings) {
setRunnerSettings(runnerSettings);
}
/**
* @deprecated Use {@link #getPestRunnerSettings()}
**/
@Deprecated
@Transient
@Override
public @NotNull PhpTestRunnerSettings getRunnerSettings() {
return super.getRunnerSettings();
}
}
================================================
FILE: src/main/java/com/pestphp/pest/configuration/PhpTestRunConfiguration.java
================================================
package com.pestphp.pest.configuration;
import com.intellij.execution.BeforeRunTask;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.openapi.project.Project;
import com.jetbrains.php.PhpTestFrameworkVersionDetector;
import com.jetbrains.php.testFramework.PhpTestFrameworkType;
import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationHandler;
import com.jetbrains.php.testFramework.run.PhpTestRunnerSettingsValidator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public abstract class PhpTestRunConfiguration extends com.jetbrains.php.testFramework.run.PhpTestRunConfiguration {
protected PhpTestRunConfiguration(Project project, ConfigurationFactory factory, String name, @NotNull PhpTestFrameworkType frameworkType, @NotNull PhpTestRunnerSettingsValidator validator, @NotNull PhpTestRunConfigurationHandler handler, @Nullable PhpTestFrameworkVersionDetector versionDetector) {
super(project, factory, name, frameworkType, validator, handler, versionDetector);
}
@Override
public void setBeforeRunTasks(@NotNull List> value) {
super.setBeforeRunTasks(value);
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/FileUtil.kt
================================================
package com.pestphp.pest
import com.intellij.psi.PsiFile
import com.intellij.testFramework.LightVirtualFile
/**
* Takes care of getting the path of a file even if it's a light file.
*/
val PsiFile.realPath: String
get() {
var virtualFile = this.viewProvider.virtualFile
if (virtualFile is LightVirtualFile && virtualFile.originalFile != null) {
virtualFile = virtualFile.originalFile
}
return virtualFile.path
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestBundle.kt
================================================
package com.pestphp.pest
import com.intellij.DynamicBundle
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
import java.util.function.Supplier
@NonNls
private const val BUNDLE = "messages.pestBundle"
object PestBundle : DynamicBundle(BUNDLE) {
@Suppress("SpreadOperator")
@JvmStatic
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = getMessage(key, *params)
@JvmStatic
fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): Supplier {
return getLazyMessage(key, *params)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestComposerConfig.kt
================================================
package com.pestphp.pest
import com.intellij.execution.configurations.ConfigurationType
import com.jetbrains.php.testFramework.PhpTestFrameworkComposerConfig
import com.pestphp.pest.configuration.PestRunConfigurationType.Companion.instance
class PestComposerConfig : PhpTestFrameworkComposerConfig(PestFrameworkType.instance, PACKAGE, RELATIVE_PATH) {
override fun getDefaultConfigName(): String {
return "phpunit.xml"
}
override fun getConfigurationType(): ConfigurationType {
return instance
}
companion object {
private const val PACKAGE = "pestphp/pest"
private const val RELATIVE_PATH = "pestphp/pest/bin/pest"
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestFrameworkType.kt
================================================
package com.pestphp.pest
import com.intellij.openapi.project.Project
import com.jetbrains.php.testFramework.PhpTestDescriptor
import com.jetbrains.php.testFramework.PhpTestFrameworkFormDecorator
import com.jetbrains.php.testFramework.PhpTestFrameworkFormDecorator.PhpDownloadableTestFormDecorator
import com.jetbrains.php.testFramework.PhpTestFrameworkType
import com.jetbrains.php.testFramework.ui.PhpTestFrameworkBaseConfigurableForm
import com.jetbrains.php.testFramework.ui.PhpTestFrameworkBySdkConfigurableForm
import com.jetbrains.php.testFramework.ui.PhpTestFrameworkConfigurableForm
import com.pestphp.pest.configuration.PestVersionDetector
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import javax.swing.Icon
/**
* Registers a framework type for PHP.
*
* This class is used to show the menu in
* `Preferences -> Languages & Frameworks -> PHP -> Test Frameworks`
*/
class PestFrameworkType : PhpTestFrameworkType() {
private val pestUrl = "https://github.com/pestphp/pest/releases"
override fun getDisplayName(): @Nls String {
return PestBundle.message("FRAMEWORK_NAME")
}
override fun getID(): String {
return ID
}
override fun getIcon(): Icon {
return PestIcons.Logo
}
override fun getDecorator(): PhpTestFrameworkFormDecorator {
return object : PhpDownloadableTestFormDecorator(pestUrl) {
override fun decorate(
project: Project,
form: PhpTestFrameworkBaseConfigurableForm<*>
): PhpTestFrameworkConfigurableForm<*> {
if (form !is PhpTestFrameworkBySdkConfigurableForm) {
form.setVersionDetector(PestVersionDetector.instance)
}
return super.decorate(project, form)
}
}
}
override fun getDescriptor(): PhpTestDescriptor {
return PestTestDescriptor
}
companion object {
@NonNls
val ID = "Pest"
val instance: PhpTestFrameworkType
get() = getTestFrameworkType(ID)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestFunctionsUtil.kt
================================================
package com.pestphp.pest
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.jetbrains.php.PhpIndex
import com.jetbrains.php.lang.psi.elements.FieldReference
import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.GroupStatement
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.PhpExpression
import com.jetbrains.php.lang.psi.elements.PhpNamespace
import com.jetbrains.php.lang.psi.elements.PhpPsiElement
import com.jetbrains.php.lang.psi.elements.Statement
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType
val PEST_TEST_CALL_TYPE = PhpType.from(
"\\Pest\\PendingCalls\\TestCall", // for Pest versions >= 2.x
"\\Pest\\PendingObjects\\TestCall" // for Pest versions 1.x
)
fun PsiElement?.isPestTestReference(isSmart: Boolean = false): Boolean {
return when (this) {
null -> false
is MethodReference -> this.isPestTestMethodReference(isSmart)
is FunctionReferenceImpl -> this.isPestTestFunction(isSmart)
else -> false
}
}
private val testNames = setOf("it", "test", "todo", "describe", "arch")
fun FunctionReferenceImpl.isPestTestFunction(isSmart: Boolean = false): Boolean {
if (this.canonicalText !in testNames) return false
return !isSmart || (this.resolveLocal().isEmpty() && PhpIndex.getInstance(project).getFunctionsByName(this.canonicalText).any { function ->
PEST_TEST_CALL_TYPE.isConvertibleFromGlobal(project, function.type)
})
}
fun FunctionReferenceImpl.isPestBeforeFunction(): Boolean {
return this.canonicalText == "beforeEach"
}
fun FunctionReferenceImpl.isPestAfterFunction(): Boolean {
return this.canonicalText == "afterEach"
}
private val allPestNames = setOf("it", "test", "todo", "beforeEach", "afterEach", "dataset", "describe", "arch")
fun FunctionReferenceImpl.isAnyPestFunction(): Boolean {
return this.canonicalText in allPestNames
}
fun FunctionReferenceImpl.isDescribeFunction(): Boolean {
return this.canonicalText == "describe"
}
fun MethodReference.isPestTestMethodReference(isSmart: Boolean = false): Boolean {
return when (val reference = classReference) {
is FunctionReferenceImpl -> reference.isPestTestFunction(isSmart)
is MethodReference -> reference.isPestTestMethodReference(isSmart)
is FieldReference -> reference.isPestTestMethodReference(isSmart)
else -> false
}
}
fun FieldReference.isPestTestMethodReference(isSmart: Boolean = false): Boolean {
return when (val reference = classReference) {
is FunctionReferenceImpl -> reference.isPestTestFunction(isSmart)
is MethodReference -> reference.isPestTestMethodReference(isSmart)
is FieldReference -> reference.isPestTestMethodReference(isSmart)
else -> false
}
}
fun PsiFile.getRoot(): List {
val element = this.firstChild
return element.children.filterIsInstance()
.mapNotNull { it.statements }
.getOrElse(
0
) { element }
.children
.filterIsInstance()
.mapNotNull { it.firstChild }
}
/**
* Traverses elements and recursively enters describe blocks, collecting items via the collector function.
*/
internal fun collectFromDescribeBlocks(
elements: List,
collector: (PhpPsiElement) -> T?
): List {
val result = mutableListOf()
for (element in elements) {
collector(element)?.let { result.add(it) }
val funcRef = element as? FunctionReferenceImpl
if (funcRef != null && funcRef.isDescribeFunction()) {
val closure = (funcRef.parameters.getOrNull(1) as? PhpExpression)?.firstChild as? Function
val body = closure?.children?.filterIsInstance()?.firstOrNull()
val statements = body?.statements?.mapNotNull { it.firstChild }?.filterIsInstance() ?: emptyList()
result.addAll(collectFromDescribeBlocks(statements, collector))
}
}
return result
}
fun PsiFile.getPestTests(isSmart: Boolean = false): Set {
return collectFromDescribeBlocks(this.getRootPhpPsiElements()) { element ->
if (element.isPestTestReference(isSmart)) element as? FunctionReference else null
}.toSet()
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestIconProvider.kt
================================================
package com.pestphp.pest
import com.intellij.ide.FileIconProvider
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.jetbrains.php.lang.psi.PhpFile
import com.pestphp.pest.features.datasets.isIndexedPestDatasetFile
import javax.swing.Icon
class PestIconProvider : FileIconProvider {
override fun getIcon(vFile: VirtualFile, flags: Int, project: Project?): Icon? {
if (project == null || DumbService.isDumb(project)) return null
val file = PsiManager.getInstance(project).findFile(vFile) as? PhpFile ?: return null
if (file.isIndexedPestTestFile()) {
return PestIcons.File
}
if (file.isIndexedPestDatasetFile()) {
return PestIcons.Dataset
}
if (file.isPestFile()) {
return PestIcons.Logo
}
return null
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestNamingUtil.kt
================================================
package com.pestphp.pest
import com.intellij.psi.PsiElement
import com.intellij.psi.util.findParentOfType
import com.intellij.psi.util.parents
import com.intellij.remote.RemoteSdkProperties
import com.jetbrains.php.config.interpreters.PhpInterpretersManagerImpl
import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression
import com.jetbrains.php.lang.psi.elements.ClassConstantReference
import com.jetbrains.php.lang.psi.elements.ClassReference
import com.jetbrains.php.lang.psi.elements.ConcatenationExpression
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.ParameterListOwner
import com.jetbrains.php.lang.psi.elements.PhpPsiElement
import com.jetbrains.php.lang.psi.elements.PhpReference
import com.jetbrains.php.lang.psi.elements.Statement
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.run.remote.PhpRemoteInterpreterManager
import com.jetbrains.php.util.pathmapper.PhpPathMapper
import com.pestphp.pest.runner.getLocationUrl
import java.util.Locale
fun FunctionReferenceImpl.getPestTestName(): String? {
val testName = getParameter(0)?.stringValue ?: return tryGetArchTestName(this)
val parent = this.findParentOfType()
val prepend = if (parent is FunctionReferenceImpl && parent.isDescribeFunction()) {
parent.getPestTestName()
} else {
""
}
return when (this.canonicalText) {
"it" -> "${prepend}it $testName"
"describe" -> "${prepend}`$testName` → "
else -> "${prepend}$testName"
}
}
private fun tryGetArchTestName(functionReference: FunctionReference): String? =
if (functionReference.canonicalText == "arch") {
getArchTestName(functionReference)
} else {
null
}
private fun getArchTestName(functionReference: FunctionReference): String {
val parents = functionReference.parents(false).takeWhile { it !is Statement }.toList()
return parents.joinToString(separator = " → ") { element ->
val name = if (element is PhpReference) element.canonicalText else element.text
val parameters = if (element is ParameterListOwner) getParametersString(element) else ""
"$name$parameters"
}
}
private fun getParametersString(element: ParameterListOwner) =
" " + when (val elem = element.parameters.firstOrNull()) {
is ArrayCreationExpression -> elem.children.filterIsInstance().joinToString(prefix = "[", postfix = "]") { it.text }
is StringLiteralExpression -> elem.text
else -> ""
}.replace("\"", "'")
val PsiElement.stringValue: String?
get() = when (this) {
is StringLiteralExpression -> this.contents
is ConcatenationExpression -> this.contents
is ClassConstantReference -> {
val classRef = this.classReference
if (classRef is ClassReference && this.isStatic && this.lastChild.text == "class") {
classRef.fqn
?.removePrefix("\\")
?.replace("\\", "\\\\")
} else null
}
else -> null
}
val ConcatenationExpression.contents: String?
get() {
val left = this.leftOperand?.stringValue
val right = this.rightOperand?.stringValue
if (left === null || right === null) {
return null
}
return left + right
}
fun PsiElement?.getPestTestName(): String? {
return when (this) {
is MethodReference -> (this.classReference as? FunctionReference)?.getPestTestName()
is FunctionReferenceImpl -> this.getPestTestName()
else -> null
}
}
fun PsiElement?.getInitialFunctionReference(): FunctionReference? {
return when (this) {
is MethodReference -> (this.classReference as? FunctionReference).getInitialFunctionReference()
is FunctionReferenceImpl -> this
else -> null
}
}
fun PsiElement.toPestTestRegex(workingDirectory: String): String? {
return this.getPestTestName()?.toPestTestRegex(
workingDirectory,
this.containingFile.virtualFile.path,
PhpPathMapper.create(this.project)
)
}
fun PsiElement.toPestFqn(): List {
val testName = this.getPestTestName() ?: return emptyList()
val file = this.containingFile.virtualFile.path
return PhpInterpretersManagerImpl.getInstance(this.project)
.interpreters
.asSequence()
.map { it.phpSdkAdditionalData }
.filter { it is RemoteSdkProperties }
.mapNotNull {
PhpRemoteInterpreterManager.getInstance()?.createPathMappings(
this.project,
it
)
}
.map { it.convertToRemote(file) }
.map { "pest_qn://$it::$testName" }
.plus("${getLocationUrl(this.containingFile)}::$testName")
.toList()
}
fun String.toPestTestRegex(rootPath: String, file: String, pathMapper: PhpPathMapper): String {
val mappedWorkingDirectory = pathMapper.getRemoteFilePath(rootPath) ?: rootPath
val mappedFile = pathMapper.getRemoteFilePath(file) ?: file
// Follow the steps for class name generation
// 1. Take the path of the PEST file from the cwd.
val fqn = mappedFile.withoutFirstFileSeparator
.removePrefix(mappedWorkingDirectory.withoutFirstFileSeparator)
.withoutFirstFileSeparator
// 2. Make the first folder's first letter uppercase.
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
// 3. Remove file extension (.php) and compound suffixes (.test, .spec, etc.) from filename.
.removeSuffix(".php")
.let { path ->
truncateAtFirstDot(path)
}
// 4. Make directory separators to namespace separators.
.replace("\\", "\\\\")
.replace("/", "\\\\")
// 5. Remove unsupported characters (keep only alphanumeric and escaped backslashes).
.replace(Regex("[^A-Za-z0-9\\\\]"), "")
// 6. Add P as a namespace before the generated namespace.
.let { "(P\\\\)?$it" }
// Allow substring matching only for "describe" block execution
val possibleEndOfLine = if (this.endsWith(" → ")) "" else "$"
// Escape characters
val testName = this
.replace(" ", "\\s")
.replace("(", "\\(")
.replace(")", "\\)")
.replace("[", "\\[")
.replace("]", "\\]")
.replace("^", "\\^")
.replace("/", "\\/")
.replace("?", "\\?")
.replace("+", "\\+")
// Match the description of a single data set
val dataSet = """(data\sset\s".*"|\(.*\))"""
return """^$fqn::$testName(\swith\s$dataSet(\s\/\s$dataSet)*(\s#\d+)?)?$possibleEndOfLine"""
}
private fun truncateAtFirstDot(path: String): String {
val lastSep = maxOf(path.lastIndexOf('/'), path.lastIndexOf('\\'))
return if (lastSep >= 0) {
val dir = path.substring(0, lastSep + 1)
val filename = path.substring(lastSep + 1).substringBefore('.')
dir + filename
}
else {
path.substringBefore('.')
}
}
val String.withoutFirstFileSeparator: String
get() {
return this.removePrefix("/").removePrefix("\\")
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestNewTestFromClassAction.kt
================================================
package com.pestphp.pest
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.php.phpunit.codeGeneration.PhpNewTestAction
import com.jetbrains.php.testFramework.PhpTestCreateInfo
open class PestNewTestFromClassAction: PhpNewTestAction(PestBundle.messagePointer("action.Pest.New.File.text"),
PestBundle.messagePointer("ACTIONS_NEW_PEST_TEST_ACTION_DESCRIPTION"),
PestIcons.Logo) {
override fun getDefaultTestCreateInfo(project: Project, locationContext: VirtualFile?): PhpTestCreateInfo {
return PestTestCreateInfo
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestPluginDisposable.kt
================================================
package com.pestphp.pest
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
/**
* The service is intended to be used instead of a project/application as a parent disposable.
*/
@Service(Service.Level.APP, Service.Level.PROJECT)
class PestPluginDisposable : Disposable {
override fun dispose() {}
companion object {
@JvmStatic
fun getInstance(): Disposable = ApplicationManager.getApplication().getService(PestPluginDisposable::class.java)
@JvmStatic
fun getInstance(project: Project): Disposable = project.getService(PestPluginDisposable::class.java)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestSettings.kt
================================================
package com.pestphp.pest
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.xmlb.XmlSerializerUtil
import com.pestphp.pest.parser.PestConfigurationFile
import com.pestphp.pest.parser.PestConfigurationFileParser
@Service(Service.Level.PROJECT)
@State(name = "PestSettings", storages = [Storage("pest.xml")])
class PestSettings : PersistentStateComponent {
var pestFilePath = "tests/Pest.php"
var preferredTestFlavor = TestFlavor.IT
enum class TestFlavor {
IT,
TEST
}
override fun getState(): PestSettings {
return this
}
override fun loadState(state: PestSettings) {
XmlSerializerUtil.copyBean(state, this)
}
private val parser = PestConfigurationFileParser(this)
fun getPestConfiguration(project: Project, virtualFile: VirtualFile? = null): PestConfigurationFile {
return parser.parse(project, virtualFile)
}
companion object {
fun getInstance(project: Project): PestSettings {
return project.service()
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestTestCreateInfo.kt
================================================
package com.pestphp.pest
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.testFramework.PhpUnitAbstractTestCreateInfo
import com.pestphp.pest.inspections.convertTestNameToSentenceCase
import javax.swing.Icon
const val INTERNAL_PEST_FILE_TEMPLATE_NAME = "Pest file from class"
object PestTestCreateInfo : PhpUnitAbstractTestCreateInfo() {
override fun getName(): String {
return "Pest"
}
override fun getTemplateName(): String {
return INTERNAL_PEST_FILE_TEMPLATE_NAME
}
override fun getIcon(): Icon {
return PestIcons.Logo
}
override fun getTestMethodText(project: Project, classFqn: String, methodName: String): String {
return "test('${convertTestNameToSentenceCase(methodName)}', function(){})"
}
override fun shouldPostprocessTemplateFile(): Boolean {
return true
}
override fun postprocessTemplateFile(file: PsiFile) {
val test = PsiTreeUtil.findChildOfType(file, FunctionReference::class.java)
test?.parent?.delete()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestTestDescriptor.kt
================================================
package com.pestphp.pest
import com.intellij.util.SmartList
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.jetbrains.php.phpunit.PhpUnitTestDescriptor
import com.jetbrains.php.testFramework.PhpTestCreateInfo
import java.util.Collections
/**
* findTests, findClasses, and findMethods return empty collections,
* since Pest tests are function calls, not methods, and therefore are not located in PHP classes
*/
object PestTestDescriptor : PhpUnitTestDescriptor() {
override fun findTests(clazz: PhpClass): MutableCollection {
return Collections.emptySet()
}
override fun findTests(method: Method): MutableCollection {
return Collections.emptySet()
}
override fun findClasses(test: PhpClass, testName: String): MutableCollection {
return Collections.emptySet()
}
override fun findMethods(testMethod: Method): MutableCollection {
return Collections.emptySet()
}
override fun getTestCreateInfos(): MutableCollection {
return SmartList(PestTestCreateInfo)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestTestFileUtil.kt
================================================
package com.pestphp.pest
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.php.lang.psi.elements.AssignmentExpression
import com.jetbrains.php.lang.psi.elements.ClassConstantReference
import com.jetbrains.php.lang.psi.elements.ClassReference
import com.jetbrains.php.lang.psi.elements.FieldReference
import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.ParameterList
import com.jetbrains.php.lang.psi.elements.Variable
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType
val CONFIGURATION_FUNCTIONS = listOf("pest", "uses")
val CONFIGURATION_METHODS = listOf("use", "uses", "extend", "extends")
fun PsiElement?.isThisVariableInPest(condition: (FunctionReferenceImpl) -> Boolean): Boolean {
if ((this as? Variable)?.name != "this") return false
var psiElement = this
while (true) {
val functionReference = getOuterFunctionReference(psiElement) ?: return false
if (condition(functionReference)) {
return true
}
psiElement = functionReference
}
}
fun PsiElement?.isTestAsThisVariableInPest(condition: (FunctionReferenceImpl) -> Boolean): Boolean {
val functionReference = this as? FunctionReference ?: return false
if (functionReference.name != "test" || !functionReference.parameters.isEmpty()) return false
return getOuterFunctionReference(this)?.let { functionReferenceImpl -> condition(functionReferenceImpl) } ?: false
}
private fun getOuterFunctionReference(element: PsiElement?): FunctionReferenceImpl? {
val closure = PsiTreeUtil.getParentOfType(element, Function::class.java)
if (closure == null || !closure.isClosure) return null
val parameterList = closure.parent?.parent as? ParameterList ?: return null
if (parameterList.parent !is FunctionReferenceImpl) return null
return parameterList.parent as FunctionReferenceImpl
}
fun PsiFile.getAllBeforeThisAssignments(): List {
return this.getRoot()
.filterIsInstance()
.filter { it.isPestBeforeFunction() }
.flatMap { it.getThisStatements() }
}
private val cacheKey = Key>>("com.pestphp.pest_assignments")
private fun FunctionReferenceImpl.getThisStatements(): List {
return CachedValuesManager.getCachedValue(this, cacheKey) {
val result = PsiTreeUtil.findChildrenOfType(
this.parameterList?.getParameter(0),
AssignmentExpression::class.java
)
.filter { ((it.variable as? FieldReference)?.classReference as? Variable)?.name == "this" }
CachedValueProvider.Result.create(result, this)
}
}
fun FunctionReference.getConfigurationPhpType(): PhpType? {
if (this.name !in CONFIGURATION_METHODS) return PhpType()
parameters.mapNotNull {
val classRef = it as? ClassConstantReference ?: return@mapNotNull null
if (classRef.name != "class") return@mapNotNull null
(classRef.classReference as? ClassReference)?.fqn
}.apply {
if (this.isEmpty()) return null
val res = PhpType()
this.forEach {
res.add(it)
}
return res
}
}
fun FunctionReference.getPestConfigurationPhpType(): PhpType? {
if (this is FunctionReferenceImpl && this.name in CONFIGURATION_FUNCTIONS) {
return this.getConfigurationPhpType()
}
val classReference = (this as? MethodReference)?.classReference ?: return null
if (classReference is FunctionReference) {
val typeFromClassRef = classReference.getPestConfigurationPhpType()
val typeFromParameters = this.getConfigurationPhpType()
return PhpType().add(typeFromParameters).add(typeFromClassRef)
}
return null
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestTestRunLineMarkerProvider.kt
================================================
package com.pestphp.pest
import com.intellij.execution.lineMarker.RunLineMarkerContributor
import com.intellij.icons.AllIcons.RunConfigurations.TestState.Run
import com.intellij.icons.AllIcons.RunConfigurations.TestState.Run_run
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.lexer.PhpTokenTypes
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.Statement
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
/**
* Adds markers on the line on the left side for running a pest specific pest test.
*/
class PestTestRunLineMarkerProvider : RunLineMarkerContributor() {
override fun getInfo(leaf: PsiElement): Info? {
if (!leaf.containingFile.isPestTestFile(isSmart = true)) {
return null
}
// Handle icons if the reference is a pest test.
if (isPestTestReference(leaf)) {
return getPestTest(
leaf.parent as FunctionReferenceImpl,
leaf.project,
)
}
// Handle icon for running all tests in the file.
if (PhpPsiUtil.isOfType(leaf, PhpTokenTypes.PHP_OPENING_TAG)) {
return withExecutorActions(Run_run)
}
return null
}
private fun isPestTestReference(leaf: PsiElement): Boolean {
if (PhpPsiUtil.isOfType(leaf, PhpTokenTypes.IDENTIFIER)) {
(leaf.parent as? FunctionReferenceImpl)?.let { functionReference ->
if (!functionReference.isAnyPestFunction()) {
return false
}
val statementChild = PhpPsiUtil.getParentOfClass(functionReference, true, Statement::class.java)?.firstChild
val outerFunctionReference = PhpPsiUtil.getParentByCondition(
statementChild,
{ it is FunctionReferenceImpl },
PhpFile.INSTANCEOF
)
if (outerFunctionReference == null || outerFunctionReference.isDescribeFunction()) {
return statementChild is FunctionReference && statementChild.isPestTestReference(isSmart = true)
}
}
}
return false
}
private fun getPestTest(reference: FunctionReferenceImpl, project: Project): Info {
val fqn = reference.toPestFqn()
val icon = fqn.firstOrNull { getTestStateIcon(it, project, false) !== Run }
?.let { getTestStateIcon(it, project, false) } ?: Run
return withExecutorActions(icon)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/PestUtil.kt
================================================
package com.pestphp.pest
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.search.ProjectScope
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.composer.configData.ComposerConfigManager
import com.jetbrains.php.lang.psi.PhpExpressionCodeFragment
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.elements.PhpNamespace
import com.jetbrains.php.lang.psi.elements.PhpPsiElement
import com.jetbrains.php.lang.psi.elements.Statement
import com.jetbrains.php.phpunit.PhpUnitUtil
import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager
import com.pestphp.pest.indexers.key
val PEST_TEST_FILE_KEY = Key>("isPestTestFile")
val PEST_TEST_FILE_SMART_KEY = Key>("smart isPestTestFile")
fun PsiFile.isPestTestFile(isSmart: Boolean = false): Boolean {
if (this !is PhpFile || this is PhpExpressionCodeFragment) return false
return CachedValuesManager.getCachedValue(this, if (isSmart) PEST_TEST_FILE_SMART_KEY else PEST_TEST_FILE_KEY) {
val isPestTestFile = this.getRootPhpPsiElements().any { psiElement -> psiElement.isPestTestReference(isSmart) }
CachedValueProvider.Result.create(isPestTestFile, this)
}
}
fun PsiFile.isIndexedPestTestFile(): Boolean {
return FileBasedIndex.getInstance().getValues(
key,
this.realPath,
ProjectScope.getProjectScope(this.project)
).isNotEmpty() && this.isPestTestFile(isSmart = true)
}
fun PsiFile.isPestConfigurationFile(): Boolean {
return PhpUnitUtil.isPhpUnitConfigurationFile(this)
}
fun Project.isPestEnabled(): Boolean {
return PhpTestFrameworkSettingsManager
.getInstance(this)
.getConfigurations(PestFrameworkType.instance)
.any { StringUtil.isNotEmpty(it.executablePath) }
}
fun PsiFile.getRootPhpPsiElements(): List {
if (this !is PhpFile) return listOf()
val element = this.firstChild
return element.children.filterIsInstance()
.mapNotNull { it.statements }
.getOrElse(
0
) { element }
.children
.filterIsInstance()
.mapNotNull { it.firstChild }
.filterIsInstance()
}
/**
* Checks if the file is the `tests/Pest.php` file.
*/
fun PsiFile.isPestFile(): Boolean {
val baseDir = getBaseDir(this.project, this.virtualFile) ?: return false
val pestFilePath = PestSettings.getInstance(this.project).pestFilePath
return this.virtualFile?.path == baseDir.path + "/" + pestFilePath
}
fun getBaseDir(project: Project, virtualFile: VirtualFile? = null): VirtualFile? {
return ComposerConfigManager.getInstance(project).getConfig(virtualFile)?.parent
?: project.guessProjectDir()
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/annotator/PestAnnotator.kt
================================================
package com.pestphp.pest.annotator
import com.intellij.codeInsight.daemon.impl.HighlightRangeExtension
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.Annotator
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.jetbrains.php.lang.psi.resolve.types.PhpParameterBasedTypeProvider
class PestAnnotator: Annotator, HighlightRangeExtension {
override fun annotate(element: PsiElement, holder: AnnotationHolder) {
if (PhpParameterBasedTypeProvider.isMeta(holder.currentAnnotationSession.file)) return
element.accept(PestAnnotatorVisitor(holder))
}
override fun isForceHighlightParents(psiFile: PsiFile): Boolean {
return false
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/annotator/PestAnnotatorVisitor.kt
================================================
package com.pestphp.pest.annotator
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.modcommand.ModCommandAction
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.lang.annotator.PhpAnnotatorVisitor.createErrorAnnotation
import com.jetbrains.php.lang.annotator.PhpDeleteElementQuickFix
import com.jetbrains.php.lang.inspections.controlFlow.PhpNavigateToElementQuickFix
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.pestphp.pest.PestBundle
import com.pestphp.pest.features.customExpectations.KEY
import com.pestphp.pest.features.customExpectations.extendName
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.getPestTests
class PestAnnotatorVisitor(
private val holder: AnnotationHolder
) : PhpElementVisitor() {
override fun visitPhpMethodReference(reference: MethodReference) {
checkDuplicateCustomExpectations(reference)
}
private fun checkDuplicateCustomExpectations(reference: MethodReference) {
if (reference !is MethodReferenceImpl) {
return
}
val extendName = reference.extendName ?: return
val duplicates = mutableListOf()
FileBasedIndex.getInstance().processValues(KEY, extendName, null, { file, offsets ->
reference.manager.findFile(file)?.let { psiFile ->
offsets.forEach { offset ->
val expectation = psiFile.findElementAt(offset)
val methodDescriptor = PhpPsiUtil.getParentOfClass(expectation, MethodReference::class.java)
if (methodDescriptor != null) {
duplicates.add(methodDescriptor)
}
}
}
true
}, GlobalSearchScope.allScope(reference.project))
if (duplicates.size > 1) {
val fixes = listOfNotNull(
PhpDeleteElementQuickFix(reference.parent, PestBundle.message("QUICK_FIX_DELETE_CUSTOM_EXPECTATION", extendName)),
getNavigateToCustomExpectationFix(duplicates, reference)
)
val builder = holder.newAnnotation(
HighlightSeverity.WARNING,
PestBundle.message("INSPECTION_DUPLICATE_CUSTOM_EXPECTATION")
).range(reference)
fixes.forEach { fix -> builder.withFix(fix.asIntention()) }
builder.create()
}
}
private fun getNavigateToCustomExpectationFix(
duplicates: List,
duplicate: MethodReference
): ModCommandAction? {
val duplicateIndex = duplicates.indexOf(duplicate)
if (duplicateIndex == -1) {
return null
}
val nextElement = duplicates[(duplicateIndex + 1) % duplicates.size]
return PhpNavigateToElementQuickFix(
nextElement,
PestBundle.message("INTENTION_NAVIGATE_TO_DUPLICATE_CUSTOM_EXPECTATION")
)
}
override fun visitPhpFile(phpFile: PhpFile) {
checkDuplicateTestNames(phpFile)
}
private fun checkDuplicateTestNames(file: PhpFile) {
file.getPestTests(isSmart = true)
.groupBy { it.getPestTestName() }
.filterKeys { it != null }
.filter { it.value.count() > 1 }
.forEach { (_, tests) ->
tests.forEachIndexed { index, test ->
val testName = test.getPestTestName() ?: return@forEachIndexed
createErrorAnnotation(holder, test, PestBundle.message("INSPECTION_DUPLICATE_TEST_NAME"),
PhpDeleteElementQuickFix(test.parent, PestBundle.message("QUICK_FIX_DELETE_TEST", testName)),
getNavigateToTestNameFix(tests, index))
}
}
}
private fun getNavigateToTestNameFix(duplicates: List, duplicateIndex: Int): ModCommandAction {
val nextElement = duplicates[(duplicateIndex + 1) % duplicates.size]
return PhpNavigateToElementQuickFix(
nextElement,
PestBundle.message("INTENTION_NAVIGATE_TO_DUPLICATE_TEST_NAME")
)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/completion/InternalMembersCompletionProvider.kt
================================================
package com.pestphp.pest.completion
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.util.ProcessingContext
import com.jetbrains.php.PhpIndex
import com.jetbrains.php.completion.PhpVariantsUtil
import com.jetbrains.php.lang.psi.elements.FieldReference
import com.jetbrains.php.lang.psi.elements.Variable
import com.pestphp.pest.isAnyPestFunction
import com.pestphp.pest.isThisVariableInPest
/**
* Adds completion for private and protected methods of `$this` variable
* when inside a pest test.
*/
class InternalMembersCompletionProvider : CompletionProvider() {
override fun addCompletions(
parameters: CompletionParameters,
context: ProcessingContext,
result: CompletionResultSet
) {
val fieldReference = parameters.position.parent as? FieldReference ?: return
val variable = fieldReference.classReference as? Variable ?: return
if (!variable.isThisVariableInPest { it.isAnyPestFunction() }) return
val phpIndex = PhpIndex.getInstance(fieldReference.project)
val classes = phpIndex.completeType(fieldReference.project, variable.type, null).types
.filter { it.startsWith("\\") }
.flatMap {
phpIndex.getAnyByFQN(it)
}
classes.flatMap { phpClass ->
phpClass.methods.filter { it.access.isProtected || (!it.access.isPrivate && it.isStatic) }
}.forEach {
result.addElement(PhpVariantsUtil.getLookupItem(it, null))
}
classes.flatMap { phpClass ->
phpClass.fields.filter { it.modifier.isProtected }
}.forEach {
result.addElement(PhpVariantsUtil.getLookupItem(it, null))
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/completion/PestCompletionContributor.kt
================================================
package com.pestphp.pest.completion
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.patterns.PlatformPatterns
import com.jetbrains.php.lang.lexer.PhpTokenTypes
import com.jetbrains.php.lang.psi.elements.FieldReference
import com.jetbrains.php.lang.psi.elements.MemberReference
/**
* Registers the completion providers
*/
private class PestCompletionContributor : CompletionContributor() {
init {
extend(
CompletionType.BASIC,
PlatformPatterns.psiElement()
.withElementType(PhpTokenTypes.IDENTIFIER)
.withParent(FieldReference::class.java),
InternalMembersCompletionProvider()
)
extend(
CompletionType.BASIC,
PlatformPatterns.psiElement()
.withElementType(PhpTokenTypes.IDENTIFIER)
.withParent(FieldReference::class.java),
ThisFieldsCompletionProvider()
)
extend(
CompletionType.BASIC,
PlatformPatterns.psiElement()
.withParent(MemberReference::class.java),
PestCustomExtensionCompletionProvider()
)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/completion/PestCustomExtensionCompletionProvider.kt
================================================
@file:Suppress("UnstableApiUsage")
package com.pestphp.pest.completion
import com.intellij.codeInsight.AutoPopupController
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.codeInsight.lookup.LookupElementRenderer
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.ProcessingContext
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.PhpIcons
import com.jetbrains.php.completion.PhpCompletionUtil
import com.jetbrains.php.completion.insert.PhpInsertHandlerUtil
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.MemberReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.pestphp.pest.features.customExpectations.KEY
import com.pestphp.pest.features.customExpectations.expectationType
import com.pestphp.pest.features.customExpectations.toMethod
class PestCustomExtensionCompletionProvider : CompletionProvider() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val memberReference = parameters.position.originalElement.parent as? MemberReference ?: return
val project = parameters.position.project
if (PhpType.intersectsGlobal(project, expectationType, memberReference.classReference?.globalType ?: PhpType.EMPTY)) {
val index = FileBasedIndex.getInstance()
index.getAllKeys(KEY, project).forEach { extensionName ->
index.processValues(KEY, extensionName, null, { file, value ->
memberReference.manager.findFile(file)?.let { psiFile ->
val cheapRenderer = CustomExpectationRenderer()
val lookupElement = LookupElementBuilder.create(extensionName)
.withRenderer(cheapRenderer)
.withExpensiveRenderer(CustomExtensionExpensiveRenderer(cheapRenderer, psiFile, value.first()))
.withInsertHandler { context, _ ->
val expectation = psiFile.findElementAt(value.first())
val methodDescriptor =
PhpPsiUtil.getParentOfClass(expectation, MethodReference::class.java)?.toMethod()
PhpInsertHandlerUtil.insertStringAtCaret(context.editor, "()")
if (methodDescriptor?.parameters?.isNotEmpty() == true) {
PhpCompletionUtil.moveCaretRelativelyWithScroll(context.editor, -1)
AutoPopupController.getInstance(project).autoPopupParameterInfo(context.editor, null)
}
}
result.addElement(lookupElement)
}
true
}, GlobalSearchScope.projectScope(project))
}
}
}
}
private class CustomExpectationRenderer : LookupElementRenderer() {
override fun renderElement(element: LookupElement, presentation: LookupElementPresentation) {
presentation.icon = PhpIcons.METHOD
presentation.itemText = element.lookupString
presentation.isItemTextBold = true
}
}
private class CustomExtensionExpensiveRenderer(
private val cheapRenderer: CustomExpectationRenderer,
private val targetFile: PsiFile,
private val expectationOffset: Int
) : LookupElementRenderer() {
override fun renderElement(element: LookupElement, presentation: LookupElementPresentation) {
cheapRenderer.renderElement(element, presentation)
val expectation = targetFile.findElementAt(expectationOffset)
PhpPsiUtil.getParentOfClass(expectation, MethodReference::class.java)?.toMethod()?.let {
val params = it.parameters.map { p ->
if (p.returnType.isEmpty) {
p.name
} else {
"${p.name}: ${p.returnType}"
}
}
presentation.tailText = "(${params.joinToString(", ")})"
presentation.typeText = it.returnType.global(targetFile.project).toString()
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/completion/ThisFieldsCompletionProvider.kt
================================================
package com.pestphp.pest.completion
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.intellij.psi.util.elementType
import com.intellij.util.ProcessingContext
import com.jetbrains.php.PhpIcons
import com.jetbrains.php.lang.lexer.PhpTokenTypes
import com.jetbrains.php.lang.psi.elements.FieldReference
import com.jetbrains.php.lang.psi.elements.Variable
import com.pestphp.pest.getAllBeforeThisAssignments
import com.pestphp.pest.isAnyPestFunction
import com.pestphp.pest.isThisVariableInPest
/**
* Adds completion for variable assignments from `beforeEach` when using `$this`
* inside a pest test.
*/
internal class ThisFieldsCompletionProvider : CompletionProvider(), GotoDeclarationHandler {
override fun addCompletions(
parameters: CompletionParameters,
context: ProcessingContext,
result: CompletionResultSet
) {
val fieldReference = parameters.position.parent as? FieldReference ?: return
val variable = fieldReference.classReference as? Variable ?: return
if (!variable.isThisVariableInPest { it.isAnyPestFunction() }) return
return (fieldReference.containingFile).getAllBeforeThisAssignments()
.filter { it.variable?.name !== null }
.forEach {
result.addElement(
LookupElementBuilder.create(it.variable!!.name!!)
.withIcon(PhpIcons.FIELD)
.withTypeText(it.type.toStringRelativized("\\"))
)
}
}
override fun getGotoDeclarationTargets(
sourceElement: PsiElement?,
offset: Int,
editor: Editor?
): Array {
if (sourceElement?.elementType != PhpTokenTypes.IDENTIFIER) {
return PsiElement.EMPTY_ARRAY
}
val fieldReference = sourceElement?.parent as? FieldReference
?: return PsiElement.EMPTY_ARRAY
if (fieldReference.classReference?.isThisVariableInPest { it.isAnyPestFunction() } != true) {
return PsiElement.EMPTY_ARRAY
}
return (fieldReference.containingFile ?: return PsiElement.EMPTY_ARRAY).getAllBeforeThisAssignments()
.filter { it.variable?.name == fieldReference.name }
.mapNotNull { it.variable }
.toTypedArray()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestDebugRunner.kt
================================================
package com.pestphp.pest.configuration
import com.jetbrains.php.testFramework.run.PhpTestDebugRunner
/**
* Add support for Php's debug runners.
*/
class PestDebugRunner private constructor() :
PhpTestDebugRunner(PestRunConfiguration::class.java) {
override fun getRunnerId(): String {
return "PestDebugRunner"
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestLocationProvider.kt
================================================
package com.pestphp.pest.configuration
import com.intellij.execution.Location
import com.intellij.execution.testframework.sm.runner.SMTestLocator
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.phpunit.PhpPsiLocationWithDataSet
import com.jetbrains.php.phpunit.PhpUnitQualifiedNameLocationProvider
import com.jetbrains.php.util.pathmapper.PhpLocalPathMapper
import com.jetbrains.php.util.pathmapper.PhpPathMapper
import com.pestphp.pest.features.parallel.convertRuntimeTestNameToRealTestName
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.getPestTests
import com.pestphp.pest.runner.LocationInfo
import java.io.File
private const val PARALLEL_EXECUTION_URL_MARKER = "eval()'d code::"
/**
* Adds support for goto test from test results.
*/
class PestLocationProvider(
val pathMapper: PhpPathMapper,
private val project: Project,
private val configurationFileRootPath: String? = null
) : SMTestLocator {
private val phpUnitLocationProvider = PhpUnitQualifiedNameLocationProvider.create(pathMapper)
override fun getLocation(
protocol: String,
path: String,
project: Project,
scope: GlobalSearchScope
): MutableList> {
val isParallelExecution = path.contains(PARALLEL_EXECUTION_URL_MARKER)
if (protocol != PROTOCOL_ID && !isParallelExecution) {
return phpUnitLocationProvider.getLocation(protocol, path, project, scope)
}
val locationInfo = if (isParallelExecution) getParallelLocationInfo(path) else getLocationInfo(path)
val element = locationInfo?.let { findElement(it, project) } ?: return mutableListOf()
return mutableListOf(
PhpPsiLocationWithDataSet(
project,
element,
getDataSet(locationInfo)
)
)
}
private fun getDataSet(locationInfo: LocationInfo): String? {
return locationInfo.testName
}
private fun getLocationInfo(link: String): LocationInfo? {
val location = link.split("::")
return resolveLocationInfo(location)
}
private fun getParallelLocationInfo(link: String): LocationInfo? {
val rawParallelLocation = link.substringAfter(PARALLEL_EXECUTION_URL_MARKER).split("::")
val location = listOfNotNull(
convertLocationHintClassNameToFileName(rawParallelLocation[0]),
rawParallelLocation.getOrNull(1)?.let { runtimeTestName -> convertRuntimeTestNameToRealTestName(runtimeTestName) }
)
return resolveLocationInfo(location)
}
private fun resolveLocationInfo(location: List): LocationInfo? {
val fileUrl = calculateFileUrl(location[0])
val testName = location.getOrNull(1)
val file = this.pathMapper.getLocalFile(fileUrl) ?: PhpLocalPathMapper(project).getLocalFile(fileUrl)
return file?.let { LocationInfo(it, testName) }
}
private fun findElement(locationInfo: LocationInfo, project: Project): PsiElement? {
return this.getLocation(
project,
locationInfo.file,
locationInfo.testName
)
}
private fun getLocation(project: Project, virtualFile: VirtualFile, testName: String?): PsiElement? {
val file = PsiManager.getInstance(project).findFile(virtualFile) ?: return null
if (testName == null) {
return file
}
return (file as PhpFile).getPestTests().firstOrNull { it.getPestTestName() == testName }
}
override fun getLocation(
stacktraceLine: String,
project: Project,
scope: GlobalSearchScope
): MutableList> {
return mutableListOf()
}
fun calculateFileUrl(locationOutput: String): String {
val pathPrefix = configurationFileRootPath ?: project.basePath
return if (pathPrefix != null && locationOutput.startsWith(pathPrefix)) {
locationOutput // for Pest versions 1.x
} else {
"$pathPrefix/${locationOutput}" // for Pest versions >= 2.x
}
}
companion object {
const val PROTOCOL_ID = "pest_qn"
}
}
private fun convertLocationHintClassNameToFileName(locationHintClassName: String): String {
return locationHintClassName.removePrefix("\\P\\").replace("\\", File.separator) + ".php"
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRerunFailedTestsAction.kt
================================================
package com.pestphp.pest.configuration
import com.intellij.execution.Executor
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.ProgramRunner
import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction
import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties
import com.intellij.openapi.ui.ComponentContainer
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.jetbrains.php.composer.configData.ComposerConfigManager
import com.jetbrains.php.config.commandLine.PhpCommandSettings
import com.pestphp.pest.PestBundle
import com.pestphp.pest.features.parallel.PestParallelProgramRunner
import com.pestphp.pest.isPestTestReference
import com.pestphp.pest.notifications.OutdatedNotification
import com.pestphp.pest.toPestTestRegex
/**
* Adds support for rerunning failed tests
*/
class PestRerunFailedTestsAction(
componentContainer: ComponentContainer,
properties: SMTRunnerConsoleProperties
) : AbstractRerunFailedTestsAction(componentContainer) {
override fun getRunProfile(environment: ExecutionEnvironment): MyRunProfile? {
val profile = myConsoleProperties.configuration
if (profile !is PestRunConfiguration) {
return null
}
val runConfiguration: PestRunConfiguration = profile
return object : MyRunProfile(runConfiguration), PestRerunProfile {
override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState? {
val peerRunConfiguration = this.peer as PestRunConfiguration
val project = peerRunConfiguration.project
val interpreter = peerRunConfiguration.interpreter ?: return null
val failed = getFailedTests(project)
.asSequence()
.filter { it.isLeaf }
.filter { it.parent != null }
.map { it.getLocation(project, GlobalSearchScope.allScope(project)) }
.mapNotNull { it?.psiElement }
.filter { it.isPestTestReference() }
.toList()
val clone: PestRunConfiguration = peerRunConfiguration.clone() as PestRunConfiguration
// If there are no failed tests found, it's prob.
// because it's an pest version before the new printer
if (failed.isEmpty()) {
OutdatedNotification().notify(
project,
PestBundle.message("NO_FAILED_TESTS_FOUND")
)
return peerRunConfiguration.getState(
environment,
clone.createCommand(
interpreter,
mutableMapOf(),
mutableListOf(),
false
),
null
)
}
val command: PhpCommandSettings = clone.createCommand(
interpreter,
mutableMapOf(),
getArgumentsFromRunner(environment.runner),
false
)
val rootPath = ComposerConfigManager.getInstance(project).getConfig(null as PsiElement?)?.parent?.path ?: command.workingDirectory
val testcases = failed.mapNotNull { it.toPestTestRegex(rootPath) }
.reduce { result, testName -> "$result|$testName" }
command.addArgument(
"--filter=/$testcases/"
)
return peerRunConfiguration.getState(
environment,
command,
null
)
}
}
}
init {
init(properties)
}
private fun getArgumentsFromRunner(pestProgramRunner: ProgramRunner<*>): MutableList {
return when (pestProgramRunner) {
is PestParallelProgramRunner -> pestProgramRunner.getArguments()
else -> mutableListOf()
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRerunProfile.kt
================================================
package com.pestphp.pest.configuration
interface PestRerunProfile
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunConfiguration.kt
================================================
package com.pestphp.pest.configuration
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.configurations.RuntimeConfigurationException
import com.intellij.execution.configurations.RuntimeConfigurationWarning
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction
import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties
import com.intellij.execution.ui.ConsoleView
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.util.PathUtil
import com.intellij.util.TextFieldCompletionProvider
import com.jetbrains.php.PhpBundle
import com.jetbrains.php.config.commandLine.PhpCommandLinePathProcessor
import com.jetbrains.php.config.commandLine.PhpCommandSettings
import com.jetbrains.php.config.interpreters.PhpInterpreter
import com.jetbrains.php.run.PhpAsyncRunConfiguration
import com.jetbrains.php.run.PhpRunUtil
import com.jetbrains.php.run.remote.PhpRemoteInterpreterManager
import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration
import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager
import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationSettings
import com.jetbrains.php.testFramework.run.PhpTestRunnerConfigurationEditor
import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings
import com.pestphp.pest.PestBundle
import com.pestphp.pest.PestFrameworkType
import com.pestphp.pest.PestIcons
import com.pestphp.pest.configuration.PestRunConfigurationProducer.Companion.VALIDATOR
import com.pestphp.pest.features.parallel.addParallelArguments
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.getPestTests
import com.pestphp.pest.runner.PestConsoleProperties
import java.util.EnumMap
import kotlin.io.path.Path
class PestRunConfiguration(project: Project, factory: ConfigurationFactory) : PhpTestRunConfiguration(
project,
factory,
PestBundle.message("FRAMEWORK_NAME"),
PestFrameworkType.instance,
VALIDATOR,
PestRunConfigurationHandler.instance,
PestVersionDetector.instance
), PhpAsyncRunConfiguration {
override fun createSettings(): PestRunConfigurationSettings {
return PestRunConfigurationSettings()
}
override fun getConfigurationEditor(): SettingsEditor {
val names = EnumMap(PhpTestRunnerSettings.Scope::class.java)
val editor = this.getConfigurationEditor(names)
editor.setRunnerOptionsDocumentation("https://pestphp.com/docs/installation")
return PestTestRunConfigurationEditor(editor, this)
}
@Throws(ExecutionException::class)
@Suppress("SwallowedException")
fun checkAndGetState(env: ExecutionEnvironment, command: PhpCommandSettings): RunProfileState? {
try {
checkConfiguration()
} catch (ignored: RuntimeConfigurationWarning) {
} catch (exception: RuntimeConfigurationException) {
throw ExecutionException(PestBundle.message("RUNTIME_CONFIGURATION_EXCEPTION_MESSAGE", exception.localizedMessage, this.name))
}
return this.getState(env, command, null)
}
override fun createMethodFieldCompletionProvider(
editor: PhpTestRunnerConfigurationEditor
): TextFieldCompletionProvider {
return object : TextFieldCompletionProvider() {
override fun addCompletionVariants(text: String, offset: Int, prefix: String, result: CompletionResultSet) {
val file = PhpRunUtil.findPsiFile(project, settings.runnerSettings.filePath)
file?.getPestTests()
?.mapNotNull { it.getPestTestName() }
?.map { LookupElementBuilder.create(it).withIcon(PestIcons.File) }
?.forEach { result.addElement(it) }
}
}
}
override fun createRerunAction(
consoleView: ConsoleView,
properties: SMTRunnerConsoleProperties
): AbstractRerunFailedTestsAction {
return PestRerunFailedTestsAction(consoleView, properties)
}
override fun createTestConsoleProperties(executor: Executor): SMTRunnerConsoleProperties {
val manager = PhpRemoteInterpreterManager.getInstance()
val pathProcessor = when (this.interpreter?.isRemote) {
true -> manager?.createPathMapper(this.project, interpreter!!.phpSdkAdditionalData)
else -> null
}
return this.createTestConsoleProperties(
executor,
pathProcessor ?: PhpCommandLinePathProcessor.LOCAL
)
}
private fun createTestConsoleProperties(
executor: Executor,
processor: PhpCommandLinePathProcessor
): PestConsoleProperties {
val pathMapper = processor.createPathMapper(this.project)
return PestConsoleProperties(
this,
executor,
PestLocationProvider(pathMapper, this.project, this.getConfigurationFileRootPath())
)
}
override fun suggestedName(): String? {
val runner = this.settings.runnerSettings
return when (val scope = runner.scope) {
PhpTestRunnerSettings.Scope.Directory -> PathUtil.getFileName(StringUtil.notNullize(runner.directoryPath))
PhpTestRunnerSettings.Scope.File -> PathUtil.getFileName(StringUtil.notNullize(runner.filePath))
PhpTestRunnerSettings.Scope.Method -> {
val file = PathUtil.getFileName(StringUtil.notNullize(runner.filePath))
"$file::${runner.methodName}"
}
PhpTestRunnerSettings.Scope.ConfigurationFile -> PathUtil.getFileName(
StringUtil.notNullize(runner.configurationFilePath)
)
else -> {
assert(false) { "Unknown scope: $scope" }
null
}
}
}
fun applyTestArguments(command: PhpCommandSettings, coverageArguments: List) {
val config = PhpTestFrameworkSettingsManager.getInstance(project)
.getOrCreateByInterpreter(PestFrameworkType.instance, interpreter, true)
?: throw ExecutionException(PestBundle.message("DIALOG_MESSAGE_COULD_NOT_FIND_PHP_INTERPRETER"))
val version = null
val workingDirectory = getWorkingDirectory(project, settings, config)
?: throw ExecutionException(PhpBundle.message("php.interpreter.base.configuration.working.directory"))
PestRunConfigurationHandler.instance.prepareCommand(
project,
command,
config.executablePath!!,
version
)
command.importCommandLineSettings(settings.commandLineSettings, workingDirectory)
fillTestRunnerArguments(
project,
workingDirectory,
settings.runnerSettings,
coverageArguments,
command,
config,
PestRunConfigurationHandler.instance
)
}
override fun createCommand(
interpreter: PhpInterpreter,
env: MutableMap,
arguments: MutableList,
withDebugger: Boolean
): PhpCommandSettings {
PestRunConfigurationHandler.instance.rootPath = getConfigurationFileRootPath()
return super.createCommand(interpreter, env, arguments, withDebugger).apply {
addParallelArguments(this@PestRunConfiguration, this)
}
}
override fun getWorkingDirectory(
project: Project,
settings: PhpTestRunConfigurationSettings,
config: PhpTestFrameworkConfiguration?
): String? {
val cli = settings.commandLineSettings
if (cli.workingDirectory?.isNotEmpty() == true) {
return cli.workingDirectory
}
val configFileRootPath = getConfigurationFileRootPath()
if (configFileRootPath.isNullOrEmpty()) {
return super.getWorkingDirectory(project, settings, config)
}
return configFileRootPath
}
private fun getConfigurationFileRootPath(): String? {
val configFile = getConfigurationFile(
settings.runnerSettings,
PhpTestFrameworkSettingsManager.getInstance(project)
.getOrCreateByInterpreter(PestFrameworkType.instance, interpreter, true)
) ?: return null
return VfsUtil.findFile(Path(configFile), false)?.parent?.path
}
val pestSettings: PestRunConfigurationSettings
get() {
return super.getSettings() as PestRunConfigurationSettings
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunConfigurationHandler.kt
================================================
package com.pestphp.pest.configuration
import com.intellij.execution.ExecutionException
import com.intellij.openapi.project.Project
import com.jetbrains.php.config.commandLine.PhpCommandSettings
import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationHandler
import com.pestphp.pest.PestBundle
import com.pestphp.pest.toPestTestRegex
class PestRunConfigurationHandler : PhpTestRunConfigurationHandler {
var rootPath: String? = null
companion object {
@JvmField
val instance = PestRunConfigurationHandler()
}
override fun getConfigFileOption(): String {
return "--configuration"
}
override fun prepareCommand(project: Project, commandSettings: PhpCommandSettings, exe: String, version: String?) {
commandSettings.setScript(exe, false)
commandSettings.addArgument("--teamcity")
commandSettings.addEnv("IDE_PEST_EXE", exe)
if (!version.isNullOrEmpty()) {
commandSettings.addEnv("IDE_PEST_VERSION", version)
}
}
@Throws(ExecutionException::class)
override fun runType(
project: Project,
phpCommandSettings: PhpCommandSettings,
type: String,
workingDirectory: String
) {
throw ExecutionException(PestBundle.message("CANNOT_RUN_PEST_WITH_TYPE_MESSAGE"))
}
override fun runDirectory(
project: Project,
phpCommandSettings: PhpCommandSettings,
directory: String,
workingDirectory: String
) {
if (directory.isEmpty()) {
return
}
phpCommandSettings.addPathArgument(directory)
}
override fun runFile(
project: Project,
phpCommandSettings: PhpCommandSettings,
file: String,
workingDirectory: String
) {
if (file.isEmpty()) {
return
}
phpCommandSettings.addPathArgument(file)
}
override fun runMethod(
project: Project,
phpCommandSettings: PhpCommandSettings,
file: String,
methodName: String,
workingDirectory: String
) {
if (file.isEmpty()) {
return
}
val pathMapper = phpCommandSettings.pathProcessor.createPathMapper(project)
val rootPath = this.rootPath ?: workingDirectory
phpCommandSettings.addPathArgument(file)
phpCommandSettings.addArgument("--filter")
phpCommandSettings.addArgument("/${methodName.toPestTestRegex(rootPath, file, pathMapper)}/")
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunConfigurationProducer.kt
================================================
package com.pestphp.pest.configuration
import com.intellij.execution.actions.ConfigurationFromContext
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.ide.highlighter.XmlFileType
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Condition
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.util.Function
import com.jetbrains.php.lang.PhpFileType
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.testFramework.run.PhpDefaultTestRunnerSettingsValidator
import com.jetbrains.php.testFramework.run.PhpTestConfigurationProducer
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.isPestConfigurationFile
import com.pestphp.pest.isPestEnabled
import com.pestphp.pest.isPestTestFile
import com.pestphp.pest.isPestTestReference
class PestRunConfigurationProducer : PhpTestConfigurationProducer(
VALIDATOR,
FILE_TO_SCOPE,
METHOD_NAMER,
METHOD
) {
override fun getConfigurationFactory(): ConfigurationFactory = PestRunConfigurationType.instance
override fun isEnabled(project: Project): Boolean = project.isPestEnabled()
override fun getWorkingDirectory(element: PsiElement): VirtualFile? {
if (element is PsiDirectory) {
return element.parentDirectory?.virtualFile
}
return element.containingFile?.containingDirectory?.virtualFile
}
companion object {
val METHOD = Condition { element: PsiElement? ->
element.isPestTestReference()
}
private val METHOD_NAMER = Function { element: PsiElement? ->
element.getPestTestName()
}
private val FILE_TO_SCOPE = Function { file: PsiFile ->
if (file.isPestTestFile()) file else null
}
val VALIDATOR = PhpDefaultTestRunnerSettingsValidator(
setOf(PhpFileType.INSTANCE, XmlFileType.INSTANCE).toList(),
{ file: PsiFile, _: String ->
file.isPestConfigurationFile() || file.isPestTestFile()
},
false,
false
)
}
override fun shouldReplace(self: ConfigurationFromContext, other: ConfigurationFromContext): Boolean {
val file = self.sourceElement as? PhpFile ?: return false
return file.isPestTestFile()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunConfigurationType.kt
================================================
package com.pestphp.pest.configuration
import com.intellij.execution.configurations.ConfigurationTypeUtil.findConfigurationType
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.configurations.SimpleConfigurationType
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NotNullLazyValue
import com.pestphp.pest.PestBundle
import com.pestphp.pest.PestIcons
class PestRunConfigurationType private constructor() :
SimpleConfigurationType(
"PestRunConfigurationType",
PestBundle.message("FRAMEWORK_NAME"),
PestBundle.message("FRAMEWORK_NAME"),
NotNullLazyValue.createValue { PestIcons.Config }
),
DumbAware {
override fun createTemplateConfiguration(project: Project): RunConfiguration {
return PestRunConfiguration(project, this)
}
companion object {
@JvmStatic
val instance: PestRunConfigurationType
get() = findConfigurationType(PestRunConfigurationType::class.java)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestRunnerSettings.kt
================================================
package com.pestphp.pest.configuration
import com.intellij.util.xmlb.annotations.Attribute
import com.intellij.util.xmlb.annotations.Tag
import com.jetbrains.php.phpunit.coverage.PhpUnitCoverageEngine.CoverageEngine
import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings
@Tag("PestRunner")
class PestRunnerSettings : PhpTestRunnerSettings() {
@Attribute("coverage_engine")
var coverageEngine: CoverageEngine = CoverageEngine.XDEBUG
@Attribute("parallel_testing_enabled")
var parallelTestingEnabled: Boolean = false
companion object {
@JvmStatic
fun fromPhpTestRunnerSettings(settings: PhpTestRunnerSettings): PestRunnerSettings {
val pestSettings = PestRunnerSettings()
pestSettings.scope = settings.scope
pestSettings.selectedType = settings.selectedType
pestSettings.directoryPath = settings.directoryPath
pestSettings.filePath = settings.filePath
pestSettings.methodName = settings.methodName
pestSettings.isUseAlternativeConfigurationFile = settings.isUseAlternativeConfigurationFile
pestSettings.configurationFilePath = settings.configurationFilePath
pestSettings.testRunnerOptions = settings.testRunnerOptions
return pestSettings
}
}
override fun equals(other: Any?): Boolean {
if (other !is PestRunnerSettings) return false
return super.equals(other) && coverageEngine == other.coverageEngine && parallelTestingEnabled == other.parallelTestingEnabled
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + coverageEngine.hashCode()
result = 31 * result + parallelTestingEnabled.hashCode()
return result
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestTestRunConfigurationEditor.kt
================================================
package com.pestphp.pest.configuration
import com.intellij.openapi.editor.ReadOnlyModificationException
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.components.JBCheckBox
import com.intellij.util.ui.UI
import com.jetbrains.php.phpunit.coverage.PhpUnitCoverageEngine.CoverageEngine
import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationEditor
import com.pestphp.pest.PestBundle
import java.lang.reflect.InvocationTargetException
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JPanel
class PestTestRunConfigurationEditor(
private val parentEditor: PhpTestRunConfigurationEditor,
settings: PestRunConfiguration
) : SettingsEditor() {
private val myMainPanel = JPanel()
private var coveragePanel = JPanel()
private var parallelPanel = JPanel()
private val coverageEngineComboBox = ComboBox(arrayOf(CoverageEngine.XDEBUG, CoverageEngine.PCOV))
private val enabledParallelTestingCheckBox = JBCheckBox()
init {
coveragePanel = UI.PanelFactory.grid().add(
UI.PanelFactory.panel(coverageEngineComboBox).withLabel(PestBundle.message("COVERAGE_ENGINE_LABEL_TEXT"))
).createPanel()
parallelPanel = UI.PanelFactory.grid().add(
UI.PanelFactory.panel(enabledParallelTestingCheckBox).withLabel(PestBundle.message("ENABLE_PARALLEL_TESTING_LABEL_TEXT"))
).createPanel()
myMainPanel.layout = BoxLayout(myMainPanel, BoxLayout.Y_AXIS)
myMainPanel.add(parentEditor.component)
myMainPanel.add(coveragePanel)
myMainPanel.add(parallelPanel)
resetEditorFrom(settings)
}
override fun createEditor(): JComponent {
return myMainPanel
}
private fun doApply(configuration: PestRunConfiguration) {
val settings = configuration.settings as PestRunConfigurationSettings
val runnerSettings = settings.pestRunnerSettings
runnerSettings.coverageEngine = coverageEngineComboBox.selectedItem as CoverageEngine
runnerSettings.parallelTestingEnabled = enabledParallelTestingCheckBox.isSelected
}
private fun doReset(configuration: PestRunConfiguration) {
val settings = configuration.settings as PestRunConfigurationSettings
val runnerSettings = settings.pestRunnerSettings
coverageEngineComboBox.selectedItem = runnerSettings.coverageEngine
enabledParallelTestingCheckBox.isSelected = runnerSettings.parallelTestingEnabled
}
override fun resetEditorFrom(settings: PestRunConfiguration) {
doReset(settings)
parentEditor.javaClass.declaredMethods.find { it.name == "resetEditorFrom" }!!.let {
it.isAccessible = true
it.invoke(parentEditor, settings)
}
}
override fun applyEditorTo(settings: PestRunConfiguration) {
parentEditor.javaClass.declaredMethods.find { it.name == "applyEditorTo" }!!.let {
it.isAccessible = true
try {
it.invoke(parentEditor, settings)
} catch (exception: InvocationTargetException) {
// In case the method throws a read only error (happens in code with me) we ignore it.
if (exception.cause is ReadOnlyModificationException) {
return@let
}
throw exception
}
}
doApply(settings)
}
override fun getSnapshot(): PestRunConfiguration {
val result = parentEditor.snapshot as PestRunConfiguration
doApply(result)
return result
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/configuration/PestVersionDetector.kt
================================================
package com.pestphp.pest.configuration
import com.intellij.execution.ExecutionException
import com.intellij.openapi.project.Project
import com.jetbrains.php.PhpTestFrameworkVersionDetector
import com.jetbrains.php.config.interpreters.PhpInterpreter
import com.pestphp.pest.PestBundle
import org.jetbrains.annotations.Nls
private val VERSION_REGEX = Regex("(?\\d+)\\.(?\\d+)\\.(?\\d+)")
private val VERSION_OPTIONS = arrayOf("--version", "--colors=never")
class PestVersionDetector : PhpTestFrameworkVersionDetector() {
override fun getPresentableName(): @Nls String {
return PestBundle.message("FRAMEWORK_NAME")
}
override fun getTitle(): String {
return PestBundle.message("GETTING_PEST_VERSION")
}
override fun getVersionOptions(): Array {
return VERSION_OPTIONS
}
public override fun parse(s: String): String {
val version = if (s.startsWith("Pest")) {
// for <2.0.0 versions
s.removePrefix("Pest").substringBefore("\n").trim()
} else {
// for 2.* versions
s.trim().removePrefix("Pest Testing Framework ").substringBeforeLast('.')
}
if (!version.matches(VERSION_REGEX)) {
throw ExecutionException(PestBundle.message("PEST_CONFIGURATION_UI_CAN_NOT_PARSE_VERSION", s))
}
return version
}
override fun getVersion(project: Project, interpreter: PhpInterpreter, executable: String?): String {
if (interpreter.isRemote) {
throw ExecutionException(PestBundle.message("PEST_VERSION_IS_NOT_SUPPORTED_FOR_REMOTE_INTERPRETER"))
}
return super.getVersion(project, interpreter, executable)
}
companion object {
val instance = PestVersionDetector()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/configuration/ConfigurationInDirectoryReferenceProvider.kt
================================================
package com.pestphp.pest.features.configuration
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.psi.PsiReferenceProvider
import com.intellij.util.ProcessingContext
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
import com.pestphp.pest.CONFIGURATION_FUNCTIONS
class ConfigurationInDirectoryReferenceProvider: PsiReferenceProvider() {
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array {
val inCall = element.parent.parent as MethodReferenceImpl
if (inCall.canonicalText != "in") {
return PsiReference.EMPTY_ARRAY
}
val usesCall = getConfigurationFunctionCall(inCall) as? FunctionReferenceImpl ?: return PsiReference.EMPTY_ARRAY
if (usesCall.canonicalText !in CONFIGURATION_FUNCTIONS) {
return PsiReference.EMPTY_ARRAY
}
val referenceSet = PhpFolderReferenceSet(element, element as StringLiteralExpression, this)
return referenceSet
.allReferences
.toList()
.toTypedArray()
}
}
fun getConfigurationFunctionCall(inCall: MethodReference): FunctionReference? {
val child = inCall.firstPsiChild
if (child !is MethodReference) {
if (child is FunctionReference) {
return child
}
return null
}
return getConfigurationFunctionCall(child)
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/configuration/ConfigurationReferenceContributor.kt
================================================
package com.pestphp.pest.features.configuration
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiReferenceContributor
import com.intellij.psi.PsiReferenceRegistrar
import com.jetbrains.php.lang.psi.elements.ParameterList
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
class ConfigurationReferenceContributor : PsiReferenceContributor() {
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(StringLiteralExpression::class.java)
.withParents(
ParameterList::class.java,
MethodReferenceImpl::class.java
),
ConfigurationInDirectoryReferenceProvider()
)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/configuration/PhpFolderReferenceSet.kt
================================================
package com.pestphp.pest.features.configuration
import com.intellij.openapi.util.Condition
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileSystemItem
import com.intellij.psi.PsiReferenceProvider
import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet
import com.jetbrains.php.lang.psi.elements.PhpPsiElement
import com.jetbrains.php.lang.psi.elements.impl.PhpFileReferenceSet
class PhpFolderReferenceSet(element: PsiElement, argument: PhpPsiElement, provider: PsiReferenceProvider) : PhpFileReferenceSet(element, argument, provider) {
override fun getReferenceCompletionFilter(): Condition {
return FileReferenceSet.DIRECTORY_FILTER
}
override fun computeDefaultContexts(): MutableCollection {
val containingFile = this.element.containingFile.originalFile
val directory = containingFile.virtualFile.parent
val fileSystemItems = toFileSystemItems(directory)
if (fileSystemItems.isNotEmpty()) {
return fileSystemItems
}
return super.computeDefaultContexts()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/CustomExpectationIndex.kt
================================================
package com.pestphp.pest.features.customExpectations
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.indexing.DataIndexer
import com.intellij.util.indexing.DefaultFileTypeSpecificInputFilter
import com.intellij.util.indexing.FileBasedIndex
import com.intellij.util.indexing.FileBasedIndexExtension
import com.intellij.util.indexing.FileContent
import com.intellij.util.indexing.ID
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.KeyDescriptor
import com.jetbrains.php.lang.PhpFileType
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
import com.jetbrains.php.lang.psi.stubs.indexes.PhpDepthLimitedRecursiveElementVisitor
import com.jetbrains.php.lang.psi.stubs.indexes.PhpInvokeCallsOffsetsIndex.IntArrayExternalizer
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntList
val KEY = ID.create("php.pest.custom_expectations")
class CustomExpectationIndex : FileBasedIndexExtension() {
override fun getName(): ID {
return KEY
}
override fun getVersion(): Int {
return 7
}
override fun getIndexer(): DataIndexer {
return DataIndexer { inputData ->
val file = inputData.psiFile
val map: MutableMap = mutableMapOf()
if (file is PhpFile) {
file.accept(object : PhpDepthLimitedRecursiveElementVisitor() {
override fun visitPhpMethodReference(reference: MethodReference) {
if (reference is MethodReferenceImpl && reference.isPestExtendReference()) {
reference.extendName?.let {
if (it !in map) {
map[it] = IntArrayList()
}
map[it]!!.add(reference.parameters[0].textOffset + 1)
}
}
}
})
}
return@DataIndexer map
}
}
override fun getKeyDescriptor(): KeyDescriptor {
return EnumeratorStringDescriptor.INSTANCE
}
override fun getValueExternalizer(): DataExternalizer {
return IntArrayExternalizer.INSTANCE
}
override fun getInputFilter(): FileBasedIndex.InputFilter {
return object : DefaultFileTypeSpecificInputFilter(PhpFileType.INSTANCE) {
override fun acceptInput(file: VirtualFile): Boolean {
return !isPestStubFile(file)
}
}
}
override fun dependsOnFileContent(): Boolean {
return true
}
private fun isPestStubFile(file: VirtualFile): Boolean {
val path = file.path
return path.contains("vendor") && path.contains("pestphp") && path.contains("stubs")
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/CustomExpectationNotifier.kt
================================================
package com.pestphp.pest.features.customExpectations
import com.intellij.psi.PsiFile
import com.intellij.util.messages.Topic
import com.pestphp.pest.features.customExpectations.generators.Method
import java.util.EventListener
interface CustomExpectationNotifier : EventListener {
companion object {
@Topic.ProjectLevel
val TOPIC = Topic.create("Custom expectation", CustomExpectationNotifier::class.java)
}
fun changedExpectation(file: PsiFile, customExpectations: List)
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/CustomExpectationParameterInfoHandler.kt
================================================
@file:Suppress("UnstableApiUsage")
package com.pestphp.pest.features.customExpectations
import com.intellij.lang.parameterInfo.CreateParameterInfoContext
import com.intellij.lang.parameterInfo.ParameterInfoHandlerWithTabActionSupport
import com.intellij.lang.parameterInfo.ParameterInfoUIContext
import com.intellij.lang.parameterInfo.UpdateParameterInfoContext
import com.intellij.model.psi.PsiSymbolReferenceService
import com.intellij.psi.PsiElement
import com.intellij.psi.tree.IElementType
import com.intellij.util.IntPair
import com.intellij.util.containers.ContainerUtil
import com.jetbrains.php.lang.PhpParameterInfoHandler
import com.jetbrains.php.lang.lexer.PhpTokenTypes
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.PhpPsiElement
import com.jetbrains.php.lang.psi.elements.Statement
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
import com.pestphp.pest.features.customExpectations.generators.Parameter
import com.pestphp.pest.features.customExpectations.symbols.PestCustomExpectationReference
import com.pestphp.pest.features.customExpectations.symbols.PestCustomExpectationSymbol
class CustomExpectationParameterInfoHandler : ParameterInfoHandlerWithTabActionSupport, PsiElement> {
override fun findElementForParameterInfo(context: CreateParameterInfoContext): FunctionReference? {
val methodReference =
PhpParameterInfoHandler.findAnchorForParameterInfo(context) as? MethodReferenceImpl ?: return null
val references = PsiSymbolReferenceService.getService().getReferences(methodReference)
val symbol = references.filterIsInstance().flatMap { it.resolveReference() }
.filterIsInstance().firstOrNull() ?: return null
context.itemsToShow = arrayOf(symbol.methodDescriptor.parameters)
return methodReference
}
override fun showParameterInfo(element: FunctionReference, context: CreateParameterInfoContext) {
context.showHint(element, element.textRange.startOffset, this)
}
override fun findElementForUpdatingParameterInfo(context: UpdateParameterInfoContext): FunctionReference? {
return PhpParameterInfoHandler.findAnchorForParameterInfo(context) as? FunctionReference
}
override fun updateParameterInfo(reference: FunctionReference, context: UpdateParameterInfoContext) {
context.setCurrentParameter(
PhpParameterInfoHandler.getCurrentParameterIndex(
reference, PhpParameterInfoHandler.getCurrentOffset(context),
actualParameterDelimiterType
)
)
}
override fun updateUI(params: List, context: ParameterInfoUIContext) {
if (params.isEmpty()) {
context.isUIComponentEnabled = false
return
}
var currentParameter = context.currentParameterIndex
if (currentParameter < 0) currentParameter = 0
val buffer = StringBuilder()
val highlightRange =
PhpParameterInfoHandler.appendParameterInfo(context, buffer, IntPair(-1, -1), currentParameter, params) { p ->
if (p.returnType.isEmpty) {
p.name
}
else {
"${p.name}: ${p.returnType}"
}
}
context.setupUIComponentPresentation(
buffer.toString(),
highlightRange.first,
highlightRange.second,
false,
false,
false,
context.defaultParameterColor
)
}
override fun getActualParameters(o: FunctionReference): Array {
val parameters = o.parameters
return parameters.copyOf()
}
override fun getActualParameterDelimiterType(): IElementType {
return PhpTokenTypes.opCOMMA
}
override fun getActualParametersRBraceType(): IElementType {
return PhpTokenTypes.chRPAREN
}
override fun getArgumentListAllowedParentClasses(): Set> {
return ContainerUtil.newHashSet>(PhpPsiElement::class.java)
}
override fun getArgListStopSearchClasses(): Set> {
return ContainerUtil.newHashSet(
Statement::class.java
)
}
override fun getArgumentListClass(): Class {
return FunctionReference::class.java
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/CustomExpectationRemoveGeneratedFileStartupActivity.kt
================================================
package com.pestphp.pest.features.customExpectations
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.php.composer.ComposerConfigListener
import com.jetbrains.php.composer.ComposerDataService
import com.jetbrains.php.composer.lib.ComposerLibraryServiceFactory
import com.pestphp.pest.PestPluginDisposable
class CustomExpectationRemoveGeneratedFileStartupActivity : ProjectActivity {
override suspend fun execute(project: Project) {
tryDeleteGeneratedExpectationFile(project)
val composerDataService = ComposerDataService.getInstance(project)
val listener = object : ComposerConfigListener {
override fun configPathChanged(oldPath: String?, newPath: String?, isWellConfigured: Boolean) {
if (newPath != null) {
tryDeleteGeneratedExpectationFile(project)
}
}
}
composerDataService.addConfigListener(listener)
Disposer.register(PestPluginDisposable.getInstance(project)) {
composerDataService.removeConfigListener(listener)
}
}
private fun tryDeleteGeneratedExpectationFile(project: Project) {
ComposerLibraryServiceFactory.getInstance(project, null as VirtualFile?).vendorDir?.findChild("Expectation.php")?.let {
runWriteAction { it.delete(null) }
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/ListMethodDataExternalizer.kt
================================================
package com.pestphp.pest.features.customExpectations
import com.intellij.util.io.DataExternalizer
import com.pestphp.pest.features.customExpectations.generators.Method
import java.io.DataInput
import java.io.DataOutput
class ListMethodDataExternalizer : DataExternalizer> {
override fun save(out: DataOutput, value: List) {
out.writeInt(value.size)
val methodExternalizer = MethodDataExternalizer()
value.forEach {
methodExternalizer.save(out, it)
}
}
override fun read(input: DataInput): List {
val size = input.readInt()
val methodExternalizer = MethodDataExternalizer()
val methods = mutableListOf()
repeat(size) {
methods.add(
methodExternalizer.read(input)
)
}
return methods
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/MethodDataExternalizer.kt
================================================
package com.pestphp.pest.features.customExpectations
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.jetbrains.php.lang.psi.stubs.indexes.StringSetDataExternalizer
import com.pestphp.pest.features.customExpectations.generators.Method
import com.pestphp.pest.features.customExpectations.generators.Parameter
import java.io.DataInput
import java.io.DataOutput
class MethodDataExternalizer : DataExternalizer {
override fun save(out: DataOutput, value: Method) {
EnumeratorStringDescriptor.INSTANCE.save(out, value.name)
var returnType = value.returnType.toString()
if (!value.returnType.isComplete) {
returnType = returnType.removeSuffix("|?")
}
EnumeratorStringDescriptor.INSTANCE.save(
out,
returnType
)
StringSetDataExternalizer.INSTANCE.save(
out,
value.parameters
.map { it.toString() }
.toSet()
)
}
override fun read(input: DataInput): Method {
val name = EnumeratorStringDescriptor.INSTANCE.read(input)
val returnType = EnumeratorStringDescriptor.INSTANCE.read(input)
val parameterString = StringSetDataExternalizer.INSTANCE.read(input)
val parameters = parameterString.reversed().map {
Parameter(
name = Regex("name='(.*?)'")
.find(it)!!
.groupValues[1],
returnType = PhpType.builder()
.add(
Regex("returnType='(.*?)'")
.find(it)!!
.groupValues[1]
).build(),
defaultValue = Regex("defaultValue='(.*)'")
.find(it)!!
.groupValues[1]
.ifEmpty { null }
)
}
return Method(
name,
PhpType().add(returnType),
parameters
)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/expectationUtil.kt
================================================
package com.pestphp.pest.features.customExpectations
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.php.PhpIndex
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.ParameterList
import com.jetbrains.php.lang.psi.elements.PhpExpression
import com.jetbrains.php.lang.psi.elements.PhpNamespace
import com.jetbrains.php.lang.psi.elements.Statement
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.Variable
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.pestphp.pest.features.customExpectations.generators.Method
import com.pestphp.pest.features.customExpectations.generators.Parameter
val expectationType = PhpType().apply {
this.add("\\Pest\\Expectation")
}
fun PhpType.isExpectation(project: Project): Boolean {
val filteredType = this.filterMixed()
if (filteredType.isEmpty) {
return false
}
return expectationType.isConvertibleFrom(
filteredType,
PhpIndex.getInstance(project)
)
}
fun Statement.isExpectation(): Boolean {
return (this.firstPsiChild as? MethodReference)?.isExpectation() == true
}
fun MethodReference.isExpectation(): Boolean {
return this.type.isExpectation(this.project)
}
fun PsiElement?.isThisVariableInExtend(): Boolean {
if ((this as? Variable)?.name != "this") return false
val closure = PsiTreeUtil.getParentOfType(this, Function::class.java)
if (closure == null || !closure.isClosure) return false
val parameterList = closure.parent?.parent as? ParameterList ?: return false
return parameterList.parent.isPestExtendReference()
}
fun PsiElement.isPestExtendReference(): Boolean {
if (this !is MethodReferenceImpl) {
return false
}
if (this.canonicalText != "extend") {
return false
}
val expectReference = this.firstChild
if (expectReference !is FunctionReferenceImpl) {
return false
}
if (expectReference.canonicalText != "expect") {
return false
}
return true
}
val MethodReference.extendName: String?
get() {
val name = this.getParameter(0) ?: return null
if (name !is StringLiteralExpression) {
return null
}
return name.contents
}
val PsiFile.customExpects: List
get() {
if (this !is PhpFile) return emptyList()
val element = this.firstChild
return element.children.filterIsInstance()
.mapNotNull { it.statements }
.getOrElse(
0
) { element }
.children
.filterIsInstance()
.mapNotNull { it.firstChild }
.filterIsInstance()
.filter { it.isPestExtendReference() }
}
fun MethodReference.toMethod(): Method? {
val extendName = this.extendName ?: return null
// Custom expectations should always have two parameters.
if(this.parameters.count() != 2) {
return null
}
val closure = (this.parameters[1] as? PhpExpression)?.firstChild as? Function
if (closure === null) {
return null
}
return Method(
extendName,
closure.type,
closure.parameters.map { parameter ->
Parameter(
parameter.name,
parameter.type,
parameter.defaultValuePresentation
)
}
)
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/externalizers/ListDataExternalizer.kt
================================================
package com.pestphp.pest.features.customExpectations.externalizers
import com.intellij.util.io.DataExternalizer
import java.io.DataInput
import java.io.DataOutput
class ListDataExternalizer(private val dataExternalizer: DataExternalizer) : DataExternalizer> {
override fun save(out: DataOutput, value: List) {
out.writeInt(value.size)
value.forEach {
dataExternalizer.save(out, it)
}
}
override fun read(input: DataInput): List {
val size = input.readInt()
val list = mutableListOf()
repeat(size) {
list.add(
dataExternalizer.read(input)
)
}
return list
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/externalizers/MethodDataExternalizer.kt
================================================
package com.pestphp.pest.features.customExpectations.externalizers
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.pestphp.pest.features.customExpectations.generators.Method
import java.io.DataInput
import java.io.DataOutput
class MethodDataExternalizer : DataExternalizer {
companion object {
val INSTANCE = MethodDataExternalizer()
}
override fun save(out: DataOutput, value: Method) {
EnumeratorStringDescriptor.INSTANCE.save(out, value.name)
PhpTypeDataExternalizer.INSTANCE.save(out, value.returnType)
ListDataExternalizer(ParameterDataExternalizer.INSTANCE).save(
out,
value.parameters
)
}
override fun read(input: DataInput): Method {
val name = EnumeratorStringDescriptor.INSTANCE.read(input)
val returnType = PhpTypeDataExternalizer.INSTANCE.read(input)
val parameters = ListDataExternalizer(ParameterDataExternalizer.INSTANCE).read(input)
return Method(
name,
returnType,
parameters
)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/externalizers/ParameterDataExternalizer.kt
================================================
package com.pestphp.pest.features.customExpectations.externalizers
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.NullableDataExternalizer
import com.pestphp.pest.features.customExpectations.generators.Parameter
import java.io.DataInput
import java.io.DataOutput
class ParameterDataExternalizer : DataExternalizer {
companion object {
val INSTANCE = ParameterDataExternalizer()
}
override fun save(out: DataOutput, value: Parameter) {
EnumeratorStringDescriptor.INSTANCE.save(out, value.name)
PhpTypeDataExternalizer.INSTANCE.save(out, value.returnType)
NullableDataExternalizer(EnumeratorStringDescriptor.INSTANCE).save(
out,
value.defaultValue
)
}
override fun read(input: DataInput): Parameter {
val name = EnumeratorStringDescriptor.INSTANCE.read(input)
val returnType = PhpTypeDataExternalizer.INSTANCE.read(input)
val defaultValue = NullableDataExternalizer(EnumeratorStringDescriptor.INSTANCE).read(input)
return Parameter(
name,
returnType,
defaultValue
)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/externalizers/PhpTypeDataExternalizer.kt
================================================
package com.pestphp.pest.features.customExpectations.externalizers
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import java.io.DataInput
import java.io.DataOutput
class PhpTypeDataExternalizer : DataExternalizer {
companion object {
val INSTANCE = PhpTypeDataExternalizer()
}
override fun save(out: DataOutput, value: PhpType) {
var endValue = value.toString()
if (!value.isComplete) {
endValue = endValue.removeSuffix("|?")
}
EnumeratorStringDescriptor.INSTANCE.save(
out,
endValue
)
}
override fun read(input: DataInput): PhpType {
val type = EnumeratorStringDescriptor.INSTANCE.read(input)
return PhpType().add(type)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/generators/ExpectationGenerator.kt
================================================
package com.pestphp.pest.features.customExpectations.generators
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileFactory
import com.jetbrains.php.lang.PhpFileType
/**
* Generates A class for expectations.
*/
class ExpectationGenerator {
val docMethods: MutableList = mutableListOf()
fun generate(project: Project): String {
return docMethods
.joinToString("\n") { methodString(it, project) }
.let { //language=InjectablePHP
"""
|) {
fun parametersAsString(project: Project): List {
return parameters.map {
var parameterAsString = "${it.returnType.global(project).toStringResolved()} $${it.name}"
if (it.defaultValue !== null) {
parameterAsString += " = ${it.defaultValue}"
}
parameterAsString
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Method
if (name != other.name) return false
if (returnType.toString() != other.returnType.toString()) return false
if (parameters != other.parameters) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + returnType.toString().hashCode()
result = 31 * result + parameters.hashCode()
return result
}
override fun toString(): String {
return "Method(name='$name', returnType=$returnType, parameters=$parameters)"
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/generators/Parameter.kt
================================================
package com.pestphp.pest.features.customExpectations.generators
import com.jetbrains.php.lang.psi.resolve.types.PhpType
class Parameter(val name: String, val returnType: PhpType, val defaultValue: String? = null) {
override fun toString(): String {
val defaultValue = defaultValue ?: ""
return "Parameter(name='$name', returnType='$returnType', defaultValue='$defaultValue')"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Parameter
if (name != other.name) return false
if (returnType.toString() != other.returnType.toString()) return false
if (defaultValue != other.defaultValue) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + returnType.toString().hashCode()
result = 31 * result + (defaultValue?.hashCode() ?: 0)
return result
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationReference.kt
================================================
@file:Suppress("UnstableApiUsage")
package com.pestphp.pest.features.customExpectations.symbols
import com.intellij.model.Symbol
import com.intellij.model.psi.PsiSymbolReference
import com.intellij.openapi.util.TextRange
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.pestphp.pest.features.customExpectations.KEY
import com.pestphp.pest.features.customExpectations.toMethod
class PestCustomExpectationReference(private val methodReference: MethodReference) : PsiSymbolReference {
override fun getElement(): MethodReference = methodReference
override fun getRangeInElement() = methodReference.rangeInElement
override fun resolveReference(): Collection {
val pestCustomExtensions = mutableListOf()
methodReference.name?.let {
val extensionName = methodReference.name!!
FileBasedIndex.getInstance()
.processValues(KEY, extensionName, null, { file, value ->
methodReference.manager.findFile(file)?.let { psiFile ->
val range = TextRange.from(value.first(), extensionName.length)
val element = psiFile.findElementAt(range.startOffset)
PhpPsiUtil.getParentOfClass(element, MethodReference::class.java)?.toMethod()?.let {
pestCustomExtensions.add(PestCustomExpectationSymbol(extensionName, psiFile, range, it))
}
}
true
}, GlobalSearchScope.allScope(methodReference.project))
}
return pestCustomExtensions
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationReferenceProvider.kt
================================================
@file:Suppress("UnstableApiUsage")
package com.pestphp.pest.features.customExpectations.symbols
import com.intellij.model.Symbol
import com.intellij.model.psi.PsiExternalReferenceHost
import com.intellij.model.psi.PsiSymbolReference
import com.intellij.model.psi.PsiSymbolReferenceHints
import com.intellij.model.psi.PsiSymbolReferenceProvider
import com.intellij.model.search.SearchRequest
import com.intellij.openapi.project.Project
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.pestphp.pest.features.customExpectations.symbols.PestCustomExpectationReferenceProvider.Companion.PEST_EXPECTATION
val PEST_EXPECTATION_TYPE: PhpType = PhpType.from(PEST_EXPECTATION)
class PestCustomExpectationReferenceProvider : PsiSymbolReferenceProvider {
companion object {
const val PEST_EXPECTATION: String = "\\Pest\\Expectation"
}
override fun getReferences(
element: PsiExternalReferenceHost,
hints: PsiSymbolReferenceHints
): Collection {
if (element is MethodReferenceImpl) {
val classReference = element.classReference
val methodName = element.name
if (methodName != null && classReference != null && "extend" != methodName &&
PhpType.intersectsGlobal(element.project, PEST_EXPECTATION_TYPE, classReference.type)
) {
// workaround till `com.intellij.lang.javascript.navigation.JSGotoDeclarationHandler#getGotoDeclarationTargets` is not fixed
if (element.multiResolve(false).isEmpty()) {
return arrayListOf(PestCustomExpectationReference(element))
}
}
}
return emptyList()
}
override fun getSearchRequests(project: Project, target: Symbol): Collection {
return (target as? PestCustomExpectationSymbol)?.let { listOf(SearchRequest.of(target.expectationName)) }
?: emptyList()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationRenameUsageSearcher.kt
================================================
@file:Suppress("UnstableApiUsage")
package com.pestphp.pest.features.customExpectations.symbols
import com.intellij.find.usages.api.PsiUsage
import com.intellij.model.Pointer
import com.intellij.model.search.LeafOccurrenceMapper
import com.intellij.model.search.SearchContext
import com.intellij.model.search.SearchService
import com.intellij.openapi.application.runReadAction
import com.intellij.refactoring.rename.api.PsiModifiableRenameUsage
import com.intellij.refactoring.rename.api.RenameUsage
import com.intellij.refactoring.rename.api.RenameUsageSearchParameters
import com.intellij.refactoring.rename.api.RenameUsageSearcher
import com.intellij.util.AbstractQuery
import com.intellij.util.Processor
import com.intellij.util.Query
import com.jetbrains.php.lang.PhpLanguage
private class PestCustomExpectationRenameUsageSearcher : RenameUsageSearcher {
override fun collectSearchRequests(parameters: RenameUsageSearchParameters): Collection> {
val targetSymbol = parameters.target as? PestCustomExpectationSymbol ?: return emptyList()
val symbolPointer: Pointer = targetSymbol.createPointer()
val usages = SearchService.getInstance()
.searchWord(parameters.project, targetSymbol.expectationName)
.caseSensitive(true)
.inContexts(SearchContext.inCode())
.inFilesWithLanguage(PhpLanguage.INSTANCE)
.inScope(parameters.searchScope)
.buildQuery(LeafOccurrenceMapper.withPointer(symbolPointer, ::findReferencesToSymbol))
val selfUsage = PestCustomExtensionDeclarationUsageQuery(
PsiModifiableRenameUsage.defaultPsiModifiableRenameUsage(targetSymbol.declarationUsage())
)
return listOf(usages.mapping {
PsiModifiableRenameUsage.defaultPsiModifiableRenameUsage(
PsiUsage.textUsage(it.element.containingFile, it.element.nameNode!!.textRange)
)
}, selfUsage)
}
}
internal class PestCustomExtensionDeclarationUsageQuery(private val targetDeclaration: T) : AbstractQuery() {
override fun processResults(p0: Processor): Boolean {
return runReadAction {
p0.process(targetDeclaration)
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationSymbol.kt
================================================
@file:Suppress("UnstableApiUsage")
package com.pestphp.pest.features.customExpectations.symbols
import com.intellij.find.usages.api.SearchTarget
import com.intellij.find.usages.api.UsageHandler
import com.intellij.model.Pointer
import com.intellij.model.Symbol
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.TextRange
import com.intellij.platform.backend.navigation.NavigationRequest
import com.intellij.platform.backend.navigation.NavigationTarget
import com.intellij.platform.backend.presentation.TargetPresentation
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.SearchScope
import com.intellij.refactoring.rename.api.RenameTarget
import com.pestphp.pest.features.customExpectations.generators.Method
class PestCustomExpectationSymbol(
@NlsSafe val expectationName: String,
val file: PsiFile,
val rangeInFile: TextRange,
val methodDescriptor: Method
) : Symbol, NavigationTarget, SearchTarget, RenameTarget {
override fun createPointer() = Pointer.fileRangePointer(
file,
TextRange(rangeInFile.startOffset - 1, rangeInFile.endOffset + 1)
) { restoredFile, restoredRange ->
// pointer doesn't survive when element is of zero range
PestCustomExpectationSymbol(
expectationName,
restoredFile,
TextRange(restoredRange.startOffset + 1, restoredRange.endOffset - 1),
methodDescriptor
)
}
override val maximalSearchScope: SearchScope
get() = GlobalSearchScope.projectScope(file.project)
override val targetName = expectationName
override fun presentation() = computePresentation()
override val usageHandler: UsageHandler
get() = UsageHandler.createEmptyUsageHandler(expectationName)
override fun equals(other: Any?) =
(other as? PestCustomExpectationSymbol)?.let { expectationName == it.expectationName } ?: false
override fun hashCode() = expectationName.hashCode()
override fun computePresentation() = TargetPresentation.builder(expectationName).presentation()
override fun navigationRequest() = NavigationRequest.sourceNavigationRequest(file, rangeInFile)
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationSymbolDeclaration.kt
================================================
@file:Suppress("UnstableApiUsage")
package com.pestphp.pest.features.customExpectations.symbols
import com.intellij.model.psi.PsiSymbolDeclaration
import com.intellij.openapi.util.TextRange
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.pestphp.pest.features.customExpectations.generators.Method
class PestCustomExpectationSymbolDeclaration(private val element: StringLiteralExpression, private val methodDescriptor: Method) : PsiSymbolDeclaration {
override fun getDeclaringElement() = element
override fun getRangeInDeclaringElement() = element.textRangeInParent
override fun getSymbol(): PestCustomExpectationSymbol {
val rangeInFile = TextRange.from(element.textRange.startOffset + 1, element.contents.length)
return PestCustomExpectationSymbol(element.contents, element.containingFile, rangeInFile, methodDescriptor)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationSymbolDeclarationProvider.kt
================================================
@file:Suppress("UnstableApiUsage")
package com.pestphp.pest.features.customExpectations.symbols
import com.intellij.model.psi.PsiSymbolDeclaration
import com.intellij.model.psi.PsiSymbolDeclarationProvider
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.elements.ParameterList
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.pestphp.pest.features.customExpectations.toMethod
class PestCustomExpectationSymbolDeclarationProvider : PsiSymbolDeclarationProvider {
override fun getDeclarations(element: PsiElement, offsetInElement: Int): Collection {
val possibleExtensionName = element as? StringLiteralExpression ?: return emptyList()
if (possibleExtensionName.parent !is ParameterList || possibleExtensionName.contents.isEmpty()) return emptyList()
val methodReference = possibleExtensionName.parent.parent as? MethodReferenceImpl ?: return emptyList()
val methodDescriptor = methodReference.toMethod() ?: return emptyList()
if (methodReference.name == "extend" &&
PhpType.intersectsGlobal(element.project, methodReference.classReference!!.type, PEST_EXPECTATION_TYPE)
) {
return listOf(PestCustomExpectationSymbolDeclaration(element, methodDescriptor))
}
return emptyList()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/customExpectations/symbols/PestCustomExpectationUsageSearcher.kt
================================================
@file:Suppress("UnstableApiUsage")
package com.pestphp.pest.features.customExpectations.symbols
import com.intellij.find.usages.api.PsiUsage
import com.intellij.find.usages.api.Usage
import com.intellij.find.usages.api.UsageSearchParameters
import com.intellij.find.usages.api.UsageSearcher
import com.intellij.model.Pointer
import com.intellij.model.search.LeafOccurrence
import com.intellij.model.search.LeafOccurrenceMapper
import com.intellij.model.search.SearchContext
import com.intellij.model.search.SearchService
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import com.intellij.util.Query
import com.jetbrains.php.lang.PhpLanguage
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.MethodReference
class PestCustomExpectationUsageSearcher : UsageSearcher {
override fun collectSearchRequests(parameters: UsageSearchParameters): Collection> {
val targetSymbol = parameters.target as? PestCustomExpectationSymbol ?: return emptyList()
val symbolPointer: Pointer = targetSymbol.createPointer()
val usages = SearchService.getInstance()
.searchWord(parameters.project, targetSymbol.expectationName).caseSensitive(true)
.inContexts(SearchContext.inCode()).inFilesWithLanguage(PhpLanguage.INSTANCE)
.inScope(parameters.searchScope)
.buildQuery(LeafOccurrenceMapper.withPointer(symbolPointer, ::findReferencesToSymbol))
.mapping { PsiUsage.textUsage(it.element.containingFile, it.element.nameNode!!.textRange) }
val selfUsage = PestCustomExtensionDeclarationUsageQuery(targetSymbol.declarationUsage())
return listOf(usages, selfUsage)
}
}
fun PestCustomExpectationSymbol.declarationUsage() = PestCustomDeclarationUsage(file, rangeInFile)
class PestCustomDeclarationUsage(
override val file: PsiFile, override val range: TextRange
) : PsiUsage {
override val declaration: Boolean
get() = true
override fun createPointer() = PsiUsage.textUsage(file, range).createPointer()
}
fun findReferencesToSymbol(
symbol: PestCustomExpectationSymbol,
leafOccurrence: LeafOccurrence
): Collection {
val methodReference = PhpPsiUtil.getParentOfClass(leafOccurrence.start, MethodReference::class.java)
if (methodReference?.nameNode == null) return emptyList()
val possibleReference = PestCustomExpectationReference(methodReference)
return if (possibleReference.resolvesTo(symbol)) listOf(possibleReference) else emptyList()
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetIndex.kt
================================================
package com.pestphp.pest.features.datasets
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.indexing.DataIndexer
import com.intellij.util.indexing.DefaultFileTypeSpecificInputFilter
import com.intellij.util.indexing.FileBasedIndex
import com.intellij.util.indexing.FileBasedIndexExtension
import com.intellij.util.indexing.FileContent
import com.intellij.util.indexing.ID
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.KeyDescriptor
import com.jetbrains.php.lang.PhpFileType
import com.jetbrains.php.lang.psi.PhpFile
import com.pestphp.pest.features.customExpectations.externalizers.ListDataExternalizer
import com.pestphp.pest.realPath
val key = ID.create>("php.pest.datasets")
/**
* Indexes all pest datasets with the following key value store
* `path/datasets/file => ['my-dataset', 'my-other-dataset']
*/
class DatasetIndex : FileBasedIndexExtension>() {
override fun getName(): ID> {
return key
}
override fun getVersion(): Int {
return 3
}
override fun getIndexer(): DataIndexer, FileContent> {
return DataIndexer { inputData ->
val file = inputData.psiFile
if (file !is PhpFile) {
return@DataIndexer mapOf()
}
val datasets = file
.getDatasets()
.mapNotNull { it.getPestDatasetName() }
if (datasets.isEmpty()) {
return@DataIndexer mapOf()
}
mapOf(
file.realPath to datasets
)
}
}
override fun getKeyDescriptor(): KeyDescriptor {
return EnumeratorStringDescriptor.INSTANCE
}
override fun getValueExternalizer(): DataExternalizer> {
return ListDataExternalizer(EnumeratorStringDescriptor.INSTANCE)
}
override fun getInputFilter(): FileBasedIndex.InputFilter {
return object : DefaultFileTypeSpecificInputFilter(PhpFileType.INSTANCE) {
override fun acceptInput(file: VirtualFile): Boolean {
return super.acceptInput(file) && file.parent.path.endsWith("/Datasets")
}
}
}
override fun dependsOnFileContent(): Boolean {
return true
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetReference.kt
================================================
package com.pestphp.pest.features.datasets
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementResolveResult
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiPolyVariantReference
import com.intellij.psi.PsiReferenceBase
import com.intellij.psi.ResolveResult
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
/**
* Used to make a reference between a string and a dataset function call
*/
class DatasetReference(
element: StringLiteralExpression
) : PsiReferenceBase(element), PsiPolyVariantReference {
override fun resolve(): PsiElement? {
return multiResolve(false).firstOrNull()?.element
}
override fun getVariants(): Array {
return getAllDatasets()
.mapNotNull { it.getPestDatasetName() }
.toTypedArray()
}
override fun multiResolve(incompleteCode: Boolean): Array {
val fileBasedIndex = FileBasedIndex.getInstance()
val datasetName = element.contents
val foundDatasets = mutableListOf()
fileBasedIndex.getAllKeys(
key,
element.project
).forEach { key ->
fileBasedIndex.processValues(
com.pestphp.pest.features.datasets.key,
key,
null,
{ file, datasets ->
if (datasetName !in datasets) {
return@processValues true
}
// Add all shared datasets which matches
PsiManager.getInstance(element.project).findFile(file)!!
.getDatasets()
.filter { it.getPestDatasetName() == datasetName }
.forEach { foundDatasets.add(it) }
true
},
GlobalSearchScope.projectScope(element.project)
)
}
// Add all local datasets which matches
element.containingFile
.getDatasets()
.filter { it.getPestDatasetName() == datasetName }
.forEach { foundDatasets.add(it) }
return foundDatasets.map { PsiElementResolveResult(it) }
.toTypedArray()
}
private fun getAllDatasets(): MutableList {
val fileBasedIndex = FileBasedIndex.getInstance()
val foundDatasets = mutableListOf()
fileBasedIndex.getAllKeys(
key,
element.project
).forEach { key ->
fileBasedIndex.processValues(
com.pestphp.pest.features.datasets.key,
key,
null,
{ file, _ ->
// Add all datasets
PsiManager.getInstance(element.project).findFile(file)!!
.getDatasets()
.forEach { foundDatasets.add(it) }
true
},
GlobalSearchScope.projectScope(element.project)
)
}
// Add all local datasets which matches
element.containingFile
.getDatasets()
.forEach { foundDatasets.add(it) }
return foundDatasets
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetReferenceContributor.kt
================================================
package com.pestphp.pest.features.datasets
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiReferenceContributor
import com.intellij.psi.PsiReferenceRegistrar
import com.jetbrains.php.lang.psi.elements.ParameterList
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
/**
* Used to register all dataset reference provider
*/
class DatasetReferenceContributor : PsiReferenceContributor() {
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(StringLiteralExpression::class.java)
.withParents(
ParameterList::class.java,
MethodReferenceImpl::class.java,
),
DatasetReferenceProvider()
)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetReferenceProvider.kt
================================================
package com.pestphp.pest.features.datasets
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.psi.PsiReferenceProvider
import com.intellij.util.ProcessingContext
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
/**
* Adds goto and reference support to string literals referring datasets
*/
class DatasetReferenceProvider : PsiReferenceProvider() {
override fun getReferencesByElement(
element: PsiElement,
context: ProcessingContext
): Array {
if (element !is StringLiteralExpression) {
return PsiReference.EMPTY_ARRAY
}
val methodReference = element.parent?.parent as? MethodReference ?: return PsiReference.EMPTY_ARRAY
if (!methodReference.isDatasetCall()) return PsiReference.EMPTY_ARRAY
return arrayOf(
DatasetReference(element)
)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/datasets/DatasetUtil.kt
================================================
package com.pestphp.pest.features.datasets
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.search.ProjectScope
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.pestphp.pest.collectFromDescribeBlocks
import com.pestphp.pest.getRootPhpPsiElements
import com.pestphp.pest.isPestTestReference
import com.pestphp.pest.realPath
fun PsiFile.isIndexedPestDatasetFile(): Boolean {
return FileBasedIndex.getInstance().getValues(
key,
this.realPath,
ProjectScope.getProjectScope(this.project)
).isNotEmpty()
}
fun PsiElement?.isPestDataset(): Boolean {
return when (this) {
is FunctionReferenceImpl -> this.isPestDatasetFunction()
else -> false
}
}
fun FunctionReferenceImpl.isPestDatasetFunction(): Boolean {
return this.canonicalText in setOf("dataset")
}
fun FunctionReferenceImpl.getPestDatasetName(): String? {
return (getParameter(0) as? StringLiteralExpression)?.contents
}
fun MethodReference.isDatasetCall() : Boolean {
if (name != "with") return false
return isPestTestReference()
}
fun PsiFile.getDatasets(): List {
return collectFromDescribeBlocks(this.getRootPhpPsiElements()) { element ->
(element as? FunctionReferenceImpl)?.takeIf { it.isPestDataset() }
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/datasets/InvalidDatasetNameCaseInspection.kt
================================================
package com.pestphp.pest.features.datasets
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.modcommand.ActionContext
import com.intellij.modcommand.ModPsiUpdater
import com.intellij.modcommand.PsiUpdateModCommandAction
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.php.lang.inspections.PhpInspection
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.PhpPsiElementFactory
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.pestphp.pest.PestBundle
import com.pestphp.pest.getInitialFunctionReference
import com.pestphp.pest.getRootPhpPsiElements
import com.pestphp.pest.goto.getDatasetUsages
import com.pestphp.pest.inspections.convertTestNameToSentenceCase
import com.pestphp.pest.inspections.isInvalidNameCase
class InvalidDatasetNameCaseInspection : PhpInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpFile(file: PhpFile) {
val localDatasets = file.getRootPhpPsiElements()
.filter { it.isPestDataset() }
.filterIsInstance()
localDatasets.groupBy { it.getPestDatasetName() }
.filterKeys { datasetName ->
datasetName != null && isInvalidNameCase(datasetName)
}
.forEach {
declareProblemType(holder, it.value)
}
}
}
}
private fun declareProblemType(holder: ProblemsHolder, datasets: List) {
datasets.mapNotNull { it.getInitialFunctionReference()?.getParameter(0) }
.filterIsInstance()
.forEach {
holder.problem(it, PestBundle.message("INSPECTION_INVALID_DATASET_NAME_CASE"))
.fix(ChangeDatasetNameCasingQuickFix(it))
.register()
}
}
private class ChangeDatasetNameCasingQuickFix(
datasetDeclarationName: StringLiteralExpression
) : PsiUpdateModCommandAction(datasetDeclarationName) {
override fun getFamilyName(): String {
return PestBundle.message("QUICK_FIX_CHANGE_DATASET_NAME_CASING")
}
override fun invoke(context: ActionContext, datasetNamePsiElement: StringLiteralExpression, updater: ModPsiUpdater) {
val sentenceCaseDatasetName = convertTestNameToSentenceCase(datasetNamePsiElement.contents)
val newNameParameter = PhpPsiElementFactory.createStringLiteralExpression(
datasetNamePsiElement.project,
sentenceCaseDatasetName,
true
)
val datasetUsages = getDatasetUsages(datasetNamePsiElement)?.map { updater.getWritable(it) }
datasetUsages?.forEach {
val testWithDataset = it as? MethodReference ?: return@forEach
val nameParameter = testWithDataset.getParameter(0) as? StringLiteralExpression ?: return@forEach
nameParameter.replace(newNameParameter.copy())
}
datasetNamePsiElement.replace(newNameParameter.copy())
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/datasets/InvalidDatasetReferenceInspection.kt
================================================
package com.pestphp.pest.features.datasets
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.lang.inspections.PhpInspection
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.pestphp.pest.PestBundle
import com.pestphp.pest.getPestTests
class InvalidDatasetReferenceInspection : PhpInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpFile(file: PhpFile) {
val localDatasets = file.getDatasets()
.mapNotNull { it.getPestDatasetName() }
// Get all shared datasets
val fileBasedIndex = FileBasedIndex.getInstance()
val sharedDatasets = fileBasedIndex.getAllKeys(key, file.project)
.map {
fileBasedIndex.getValues(
key,
it,
GlobalSearchScope.projectScope(file.project)
)
}
.flatten()
.flatten()
file.getPestTests()
// Has to be a method reference, as else there is no dataset
.asSequence()
.filterIsInstance()
.filter { it.name == "with" }
.mapNotNull { it.parameters.getOrNull(0) }
.filterIsInstance()
.filter { it.contents !in localDatasets && it.contents !in sharedDatasets }
.toList()
.forEach {
declareProblemType(
holder,
it
)
}
}
}
}
@Suppress("SpreadOperator")
private fun declareProblemType(holder: ProblemsHolder, datasetName: StringLiteralExpression) {
holder.registerProblem(
datasetName,
PestBundle.message("INSPECTION_INVALID_DATASET_REFERENCE"),
*LocalQuickFix.EMPTY_ARRAY
)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/parallel/PestParallelProgramRunner.kt
================================================
package com.pestphp.pest.features.parallel
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.GenericProgramRunner
import com.intellij.execution.runners.RunContentBuilder
import com.intellij.execution.testframework.sm.runner.SMTestProxy.SMRootTestProxy
import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.notification.NotificationType
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.php.PhpBundle
import com.jetbrains.php.config.commandLine.PhpCommandSettings
import com.jetbrains.php.config.commandLine.PhpCommandSettingsBuilder
import com.jetbrains.php.phpunit.PhpUnitUtil
import com.jetbrains.php.testFramework.PhpTestFrameworkSettingsManager
import com.pestphp.pest.PestBundle
import com.pestphp.pest.PestFrameworkType
import com.pestphp.pest.configuration.PestRerunProfile
import com.pestphp.pest.configuration.PestRunConfiguration
import com.pestphp.pest.configuration.PestVersionDetector
import com.pestphp.pest.statistics.PestUsagesCollector
internal val PEST_PARALLEL_ARGUMENTS = mutableListOf("--parallel", "--log-teamcity", "php://stdout")
class PestParallelProgramRunner : GenericProgramRunner() {
companion object {
const val RUNNER_ID: String = "PestParallelRunner"
}
override fun canRun(executorId: String, profile: RunProfile): Boolean =
executorId == PestParallelTestExecutor.EXECUTOR_ID && profile is PestRunConfiguration
override fun doExecute(state: RunProfileState, environment: ExecutionEnvironment): RunContentDescriptor? {
PestUsagesCollector.logParallelTestExecution(environment.project)
val executionResult = if (environment.runProfile is PestRerunProfile) {
state.execute(environment.executor, this)
} else {
val runConfiguration = environment.runProfile as? PestRunConfiguration
?: throw ExecutionException(PestBundle.message("PEST_PARALLEL_IS_NOT_SUPPORTED_FOR_SELECTED_RUN_PROFILE"))
val command = createPestParallelCommand(runConfiguration)
runConfiguration.checkAndGetState(environment, command)?.execute(environment.executor, this)
}
if (executionResult == null) throw ExecutionException(PhpBundle.message("execution.result.is.null"))
val contentDescriptor = RunContentBuilder(executionResult, environment).showRunContent(environment.contentToReuse)
postprocessExecutionResult(contentDescriptor, environment, PestBundle.message("PARALLEL_TESTING_IS_SUPPORTED_FROM_VERSION_2"))
return contentDescriptor
}
override fun getRunnerId(): String = RUNNER_ID
fun getArguments(): MutableList = PEST_PARALLEL_ARGUMENTS
}
internal fun createPestParallelCommand(runConfiguration: PestRunConfiguration): PhpCommandSettings {
FileDocumentManager.getInstance().saveAllDocuments()
val interpreter = runConfiguration.interpreter ?: throw ExecutionException(PhpCommandSettingsBuilder.getInterpreterNotFoundError())
return runConfiguration.createCommand(
interpreter,
mutableMapOf(),
if (executeInParallel(runConfiguration)) mutableListOf() else PEST_PARALLEL_ARGUMENTS,
false
)
}
fun postprocessExecutionResult(
contentDescriptor: RunContentDescriptor,
environment: ExecutionEnvironment,
@NlsSafe versionRequirement: String,
) {
val processHandler = contentDescriptor.processHandler
processHandler?.addProcessListener(object : ProcessListener {
override fun processTerminated(event: ProcessEvent) {
val executionConsole = contentDescriptor.executionConsole as? SMTRunnerConsoleView ?: return
val rootProxy = executionConsole.resultsViewer.root as? SMRootTestProxy ?: return
if (rootProxy.isEmptySuite && !rootProxy.isTestsReporterAttached) {
handleEmptySuite(environment, versionRequirement)
}
}
})
}
private fun handleEmptySuite(
environment: ExecutionEnvironment,
@NlsSafe versionRequirement: String,
) {
val profile = environment.runProfile as PestRunConfiguration
val project = profile.project
val interpreter = profile.interpreter ?: return
val config = PhpTestFrameworkSettingsManager.getInstance(project).getOrCreateByInterpreter(
PestFrameworkType.instance, interpreter, profile.getBaseFile(null, interpreter), true
) ?: return
val version = if (!interpreter.isRemote) {
PestVersionDetector.instance.getVersion(project, interpreter, config.executablePath)
} else {
null
}
createAndShowNotification(environment, versionRequirement, version)
}
private fun createAndShowNotification(
environment: ExecutionEnvironment,
@NlsSafe versionRequirement: String,
version: String?,
) {
PhpUnitUtil.getNotificationGroup().createNotification(
versionRequirement,
NotificationType.ERROR
).apply {
version?.let {
setTitle(PestBundle.message("CURRENT_PEST_VERSION_IS", it))
}
setSuggestionType(true)
notify(environment.project)
}
}
internal fun executeInParallel(runConfiguration: RunConfiguration): Boolean {
return runConfiguration is PestRunConfiguration && runConfiguration.pestSettings.pestRunnerSettings.parallelTestingEnabled
}
fun addParallelArguments(runConfiguration: PestRunConfiguration, command: PhpCommandSettings) {
if (executeInParallel(runConfiguration)) {
PestUsagesCollector.logParallelTestExecution(runConfiguration.project)
command.addArguments(PEST_PARALLEL_ARGUMENTS)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/parallel/PestParallelSMTEventsAdapter.kt
================================================
package com.pestphp.pest.features.parallel
import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsAdapter
import com.intellij.execution.testframework.sm.runner.SMTestProxy
class PestParallelSMTEventsAdapter : SMTRunnerEventsAdapter() {
override fun onSuiteStarted(suite: SMTestProxy) {
suite.setPresentableName(convertSuiteNameToClassName(suite.name))
}
override fun onTestStarted(test: SMTestProxy) {
test.setPresentableName(convertRuntimeTestNameToRealTestName(test.name))
}
}
private const val PLACEHOLDER = " "
internal fun convertRuntimeTestNameToRealTestName(runtimeTestName: String): String =
runtimeTestName
.removePrefix("__pest_evaluable_")
.replace("__→_", " → ")
.replace("__", PLACEHOLDER)
.replace("_", " ")
.replace(PLACEHOLDER, "_")
private fun convertSuiteNameToClassName(suiteName: String): String =
suiteName.removePrefix("P\\")
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/parallel/PestParallelTestExecutor.kt
================================================
package com.pestphp.pest.features.parallel
import com.intellij.execution.Executor
import com.intellij.icons.AllIcons
import com.intellij.openapi.util.IconLoader.getDisabledIcon
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.util.text.TextWithMnemonic
import com.intellij.openapi.wm.ToolWindowId
import com.jetbrains.php.PhpIcons
import com.pestphp.pest.PestBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import javax.swing.Icon
class PestParallelTestExecutor : Executor() {
companion object {
const val EXECUTOR_ID: @NonNls String = "PestParallelTestExecutor"
const val CONTEXT_ACTION_ID: @NonNls String = "PestParallelRun"
}
override fun getToolWindowId(): String = ToolWindowId.RUN
override fun getToolWindowIcon(): Icon = AllIcons.Toolwindows.ToolWindowRun
override fun getIcon(): Icon = PhpIcons.RUN_PARA_TEST
override fun getRerunIcon(): Icon = AllIcons.Actions.Rerun
override fun getDisabledIcon(): Icon = getDisabledIcon(icon)
override fun getDescription(): String = PestBundle.message("ACTION_RUN_SELECTED_CONFIGURATION_WITH_PARALLEL_DESCRIPTION")
override fun getActionName(): String = PestBundle.message("ACTION_PEST_PARALLEL_TEXT")
override fun getId(): String = EXECUTOR_ID
override fun getStartActionText(): @Nls(capitalization = Nls.Capitalization.Title) String = PestBundle.message("RUN_PEST_WITH_PARALLEL")
override fun getStartActionText(configurationName: String): String {
val configName = if (StringUtil.isEmpty(configurationName)) "" else " '${shortenNameIfNeeded(configurationName)}'"
return TextWithMnemonic.parse(PestBundle.message("RUN_S_WITH_PARALLEL")).replaceFirst("%s", configName).toString()
}
override fun getContextActionId(): String = CONTEXT_ACTION_ID
override fun getHelpId(): String? = null
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/snapshotTesting/SnapshotLineMarkerProvider.kt
================================================
package com.pestphp.pest.features.snapshotTesting
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerProvider
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder
import com.intellij.icons.AllIcons
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.lexer.PhpTokenTypes
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.PhpUse
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.pestphp.pest.PestBundle
private class SnapshotLineMarkerProvider : RelatedItemLineMarkerProvider() {
override fun collectNavigationMarkers(
element: PsiElement,
result: MutableCollection>,
) {
if (!PhpPsiUtil.isOfType(element, PhpTokenTypes.IDENTIFIER)) {
return
}
val functionReference = element.parent as? FunctionReferenceImpl ?: return
if (!functionReference.isSnapshotAssertionCall) {
return
}
if (functionReference.parent is PhpUse) {
return
}
val snapshotFiles = functionReference.snapshotFiles
val builder = NavigationGutterIconBuilder.create(AllIcons.Nodes.DataSchema)
.setTargets(snapshotFiles)
.setTooltipText(PestBundle.message("TOOLTIP_NAVIGATE_TO_SNAPSHOT_FILES"))
result.add(builder.createLineMarkerInfo(element))
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/features/snapshotTesting/SnapshotUtil.kt
================================================
package com.pestphp.pest.features.snapshotTesting
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.util.parentOfType
import com.jetbrains.php.codeInsight.controlFlow.PhpControlFlowUtil
import com.jetbrains.php.codeInsight.controlFlow.PhpInstructionProcessor
import com.jetbrains.php.codeInsight.controlFlow.instructions.PhpCallInstruction
import com.jetbrains.php.lang.psi.elements.impl.FunctionImpl
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.isPestTestReference
val snapshotAssertionNames = listOf(
"assertMatchesSnapshot",
"assertMatchesFileHashSnapshot",
"assertMatchesFileSnapshot",
"assertMatchesHtmlSnapshot",
"assertMatchesJsonSnapshot",
"assertMatchesObjectSnapshot",
"assertMatchesTextSnapshot",
"assertMatchesXmlSnapshot",
"assertMatchesYamlSnapshot",
)
val FunctionReferenceImpl.isSnapshotAssertionCall: Boolean
get() {
return snapshotAssertionNames.contains(this.name)
}
val FunctionReferenceImpl.snapshotFiles: List
get() {
val pestBody = this.parentOfType() ?: return emptyList()
// Make sure we are inside a pest test
val pestTestReference = pestBody.parent?.parent?.parent ?: return emptyList()
if (!pestTestReference.isPestTestReference()) {
return emptyList()
}
val snapshotDirectory = this.project.guessProjectDir()
?.findFileByRelativePath("tests/__snapshots__") ?: return emptyList()
val testFileName = this.containingFile.name.removeSuffix(".php")
val testName = pestTestReference.getPestTestName() ?: return emptyList()
val snapshotFiles = mutableListOf()
val snapshotCalls = pestBody.getSnapshotCallNumber(this)
ProjectFileIndex.getInstance(this.project)
.iterateContentUnderDirectory(
snapshotDirectory
) {
val psiFile = PsiManager.getInstance(this.project)
.findFile(it) ?: return@iterateContentUnderDirectory true
if (!psiFile.isSnapshotFile(
testName,
testFileName,
snapshotCalls
)
) {
return@iterateContentUnderDirectory true
}
snapshotFiles.add(psiFile)
true
}
return snapshotFiles
}
private fun PsiFile.isSnapshotFile(testName: String, testFileName: String, snapshotCall: Int): Boolean {
val snapshotFileName = this.virtualFile.nameWithoutExtension
this.virtualFile.extension ?: return false
val testNameUnderscore = testName.replace(' ', '_')
if (!snapshotFileName.startsWith("${testFileName}__$testNameUnderscore")) {
return false
}
if (!snapshotFileName.endsWith("__$snapshotCall")) {
return false
}
return true
}
private fun FunctionImpl.getSnapshotCallNumber(snapshotFunctionReference: FunctionReferenceImpl): Int {
var snapshotCalls = 0
val processor: PhpInstructionProcessor = object : PhpInstructionProcessor() {
override fun processPhpCallInstruction(instruction: PhpCallInstruction): Boolean {
val functionReference = instruction.functionReference
if (functionReference !is FunctionReferenceImpl) {
return super.processPhpCallInstruction(instruction)
}
if (!functionReference.isSnapshotAssertionCall) {
return super.processPhpCallInstruction(instruction)
}
snapshotCalls++
if (PsiManager.getInstance(functionReference.project).areElementsEquivalent(
functionReference,
snapshotFunctionReference
)
) {
return false
}
return super.processPhpCallInstruction(instruction)
}
}
val flow = controlFlow
PhpControlFlowUtil.processSuccessors(flow.entryPoint, false, processor)
return snapshotCalls
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/goto/PestDatasetUsagesGotoHandler.kt
================================================
package com.pestphp.pest.goto
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.PsiSearchHelper
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.Processor
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.pestphp.pest.features.datasets.isDatasetCall
import com.pestphp.pest.features.datasets.isPestDatasetFunction
fun getDatasetUsages(literal: StringLiteralExpression): Array? {
val function = literal.parent?.parent as? FunctionReferenceImpl ?: return null
if (!function.isPestDatasetFunction()) return null
val searchHelper = PsiSearchHelper.getInstance(literal.project)
val result = mutableListOf()
val datasetName = literal.contents
val processor = Processor { psiFile: PsiFile ->
result.addAll(
PsiTreeUtil.findChildrenOfType(psiFile, StringLiteralExpression::class.java).filter {
it.contents == datasetName
}.mapNotNull {
it.parent?.parent as? MethodReference
}.filter {
it.isDatasetCall()
}
)
true
}
searchHelper.processAllFilesWithWordInLiterals(
datasetName,
GlobalSearchScope.allScope(literal.project),
processor,
)
return result.toTypedArray()
}
class PestDatasetUsagesGotoHandler : GotoDeclarationHandler {
override fun getGotoDeclarationTargets(
sourceElement: PsiElement?,
offset: Int,
editor: Editor?
): Array? {
val literal = sourceElement?.parent as? StringLiteralExpression ?: return null
return getDatasetUsages(literal)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/goto/PestGotoTargetPresentationProvider.kt
================================================
package com.pestphp.pest.goto
import com.intellij.codeInsight.navigation.GotoTargetPresentationProvider
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.backend.presentation.TargetPresentation
import com.intellij.psi.PsiElement
import com.pestphp.pest.PestIcons
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.isPestTestReference
class PestGotoTargetPresentationProvider: GotoTargetPresentationProvider {
override fun getTargetPresentation(element: PsiElement, differentNames: Boolean): TargetPresentation? {
if (element.isPestTestReference()) {
@NlsSafe val pestTestName = element.getPestTestName()
return TargetPresentation.builder(pestTestName ?: element.containingFile.name)
.containerText(element.containingFile?.presentation?.locationString)
.icon(PestIcons.Logo)
.presentation()
}
return null
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/goto/PestTestFinder.kt
================================================
package com.pestphp.pest.goto
import com.intellij.openapi.util.Pair
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.testIntegration.TestFinder
import com.intellij.testIntegration.TestFinderHelper
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.PhpIndex
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.getPestTests
import com.pestphp.pest.indexers.key
import com.pestphp.pest.inspections.convertTestNameToSentenceCase
import com.pestphp.pest.isPestTestFile
class PestTestFinder : TestFinder {
/**
* @return methods if the given element is a psi child of Pest function call,
* classes otherwise
*/
override fun findClassesForTest(element: PsiElement): Collection {
val classes = PhpIndex.getInstance(element.project)
.getClassesByNameInScope(
element.containingFile.name.removeSuffix("Test.php"),
GlobalSearchScope.projectScope(element.project)
)
val testName = PsiTreeUtil.getNonStrictParentOfType(element, FunctionReference::class.java)
?.getPestTestName()
?.split(" ")
?.joinToString("")
?: return classes
val methodsAndProximityScores = classes.flatMap { phpClass -> phpClass.ownMethods.toList() }
.filter { method -> testName.contains(method.name, ignoreCase = true) }
.map { method -> Pair(method, TestFinderHelper.calcTestNameProximity(method.name, testName)) }
return if (!methodsAndProximityScores.isEmpty())
TestFinderHelper.getSortedElements(methodsAndProximityScores, true)
else
classes
}
override fun findSourceElement(from: PsiElement): PsiElement? {
return from.containingFile
}
override fun isTest(element: PsiElement): Boolean {
if (element is PhpClass) return false
return element.containingFile.isPestTestFile()
}
override fun findTestsForClass(element: PsiElement): Collection {
val parent = PsiTreeUtil.getNonStrictParentOfType(element, PhpClass::class.java, Method::class.java) ?: return arrayListOf()
return when (parent) {
is PhpClass -> findTestFilesForClass(parent)
is Method -> findTestsForMethod(parent)
else -> arrayListOf()
}
}
private fun findTestsForMethod(method: Method): List {
val phpClass = method.containingClass ?: return emptyList()
val sentenceCaseMethodName = convertTestNameToSentenceCase(method.name)
val testsAndProximityScores = findTestFilesForClass(phpClass)
.flatMap { psiFile ->
psiFile.getPestTests().mapNotNull { test ->
val testName = test.getPestTestName() ?: return@mapNotNull null
val sentenceCaseTestName = if (testName.contains(' ')) testName else convertTestNameToSentenceCase(testName)
if (sentenceCaseTestName.contains(sentenceCaseMethodName, ignoreCase = true)) {
Pair(test, TestFinderHelper.calcTestNameProximity(sentenceCaseMethodName, sentenceCaseTestName))
} else {
null
}
}
}
return testsAndProximityScores.sortedBy { it.second }.map { it.first }
}
private fun findTestFilesForClass(phpClass: PhpClass): List {
return FileBasedIndex.getInstance().getAllKeys(
key,
phpClass.project
).filter { testClassName -> testClassName.contains(phpClass.name) }
.flatMap { testClassName ->
FileBasedIndex.getInstance().getContainingFiles(
key,
testClassName,
GlobalSearchScope.projectScope(phpClass.project)
)
}
.mapNotNull { testFile -> phpClass.manager.findFile(testFile) }
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/goto/PestTestGoToSymbolContributor.kt
================================================
package com.pestphp.pest.goto
import com.intellij.ide.projectView.PresentationData
import com.intellij.navigation.ChooseByNameContributor
import com.intellij.navigation.ItemPresentation
import com.intellij.navigation.NavigationItem
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiManager
import com.intellij.psi.search.ProjectScope
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.PhpPresentationUtil
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.pestphp.pest.PestIcons
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.getPestTests
import com.pestphp.pest.indexers.key
/**
* Adds support for navigating to pest tests via the symbol searching
*/
class PestTestGoToSymbolContributor : ChooseByNameContributor {
override fun getNames(project: Project, includeNonProjectItems: Boolean): Array {
val index = FileBasedIndex.getInstance()
return index
.getAllKeys(key, project)
.flatMap {
index.getValues(
key,
it,
when {
includeNonProjectItems -> ProjectScope.getAllScope(project)
else -> ProjectScope.getProjectScope(project)
}
)
}
.flatten()
.distinct()
.toTypedArray()
}
override fun getItemsByName(
name: String,
pattern: String,
project: Project,
includeNonProjectItems: Boolean
): Array {
val index = FileBasedIndex.getInstance()
val psiManager = PsiManager.getInstance(project)
return index
.getAllKeys(key, project)
.flatMap { fileName ->
val hasName = index.getValues(
key,
fileName,
when {
includeNonProjectItems -> ProjectScope.getAllScope(project)
else -> ProjectScope.getProjectScope(project)
}
).flatten()
.contains(name)
if (!hasName) {
return@flatMap emptyList()
}
index.getContainingFiles(
key,
fileName,
when {
includeNonProjectItems -> ProjectScope.getAllScope(project)
else -> ProjectScope.getProjectScope(project)
}
)
}.mapNotNull { psiManager.findFile(it) }
.flatMap { it.getPestTests() }
.filter { it.getPestTestName().equals(name) }
.map { functionReference ->
val location = PhpPresentationUtil.getPresentablePathForFile(
functionReference.containingFile.virtualFile,
functionReference.project
)
val presentation = PresentationData(
functionReference.getPestTestName(),
location,
PestIcons.Logo,
null,
)
PestTestFunctionReference(functionReference, presentation)
}
.toTypedArray()
}
class PestTestFunctionReference(private val functionReference: FunctionReference,
private val presentation: ItemPresentation) : NavigationItem {
override fun getPresentation(): ItemPresentation = presentation
override fun navigate(requestFocus: Boolean) = functionReference.navigate(requestFocus)
override fun canNavigate(): Boolean = functionReference.canNavigate()
override fun canNavigateToSource(): Boolean = canNavigateToSource()
override fun getName(): String? {
return functionReference.getPestTestName()
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/indexers/PestTestIndex.kt
================================================
package com.pestphp.pest.indexers
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.indexing.DataIndexer
import com.intellij.util.indexing.DefaultFileTypeSpecificInputFilter
import com.intellij.util.indexing.FileBasedIndex
import com.intellij.util.indexing.FileBasedIndexExtension
import com.intellij.util.indexing.FileContent
import com.intellij.util.indexing.ID
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.KeyDescriptor
import com.jetbrains.php.lang.PhpFileType
import com.jetbrains.php.lang.psi.stubs.indexes.StringSetDataExternalizer
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.getPestTests
import com.pestphp.pest.isPestTestFile
import com.pestphp.pest.realPath
val key = ID.create>("php.pest")
/**
* Indexes all pest test files with the following key-value store
* `path/pest-test-file-name => ['it test', 'it should work']`
* Note that php files with pest-like named functions are indexed as well
*/
class PestTestIndex : FileBasedIndexExtension>() {
override fun getName(): ID> {
return key
}
override fun getVersion(): Int {
return 5
}
override fun dependsOnFileContent(): Boolean {
return true
}
override fun getIndexer(): DataIndexer, FileContent> {
return DataIndexer { inputData ->
val file = inputData.psiFile
if (!file.isPestTestFile()) {
return@DataIndexer mapOf()
}
val map = HashMap>()
map[file.realPath] = file.getPestTests()
.mapNotNull { it.getPestTestName() }
.toSet()
return@DataIndexer map
}
}
override fun getInputFilter(): FileBasedIndex.InputFilter {
return object : DefaultFileTypeSpecificInputFilter(PhpFileType.INSTANCE) {
override fun acceptInput(file: VirtualFile): Boolean {
return super.acceptInput(file) && file.path.lowercase().contains("test")
}
}
}
override fun getKeyDescriptor(): KeyDescriptor {
return EnumeratorStringDescriptor.INSTANCE
}
override fun getValueExternalizer(): DataExternalizer> {
return StringSetDataExternalizer.INSTANCE
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/inspections/ChangeMultipleExpectCallsToChainableQuickFix.kt
================================================
package com.pestphp.pest.inspections
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.jetbrains.php.lang.psi.PhpPsiElementFactory
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.Statement
import com.pestphp.pest.PestBundle
import com.pestphp.pest.features.customExpectations.isExpectation
class ChangeMultipleExpectCallsToChainableQuickFix : LocalQuickFix {
override fun getFamilyName(): String {
return PestBundle.message("QUICK_FIX_CHANGE_MULTIPLE_EXPECT_INTO_CHAIN")
}
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
var statement = descriptor.psiElement as? Statement ?: return
var expectCall = statement.firstPsiChild as? MethodReference ?: return
var replaceExpectCall = expectCall
if (!expectCall.isExpectation()) {
return
}
// Find the first expect call in the group
while ((statement.prevPsiSibling as? Statement)?.isExpectation() == true) {
statement = statement.prevPsiSibling as Statement
expectCall = statement.firstPsiChild as MethodReference
replaceExpectCall = expectCall
}
// Loop over all the next statement and merge together to one expect cal..
var nextSibling = statement.nextPsiSibling as? Statement
while (nextSibling != null) {
val siblingMethodReference = nextSibling.firstPsiChild as? MethodReference ?: break
if (!siblingMethodReference.isExpectation()) {
break
}
// Replace expect with and on the next call.
replaceExpectCall = PhpPsiElementFactory.createMethodReference(
project,
replaceExpectCall.text
+ "\n->"
+ siblingMethodReference.text.replaceFirst("expect", "and")
)
val oldSibling = nextSibling
nextSibling = nextSibling.nextPsiSibling as? Statement
oldSibling.delete()
}
expectCall.replace(replaceExpectCall)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/inspections/ChangeTestNameCasingQuickFix.kt
================================================
package com.pestphp.pest.inspections
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.intellij.util.text.NameUtilCore
import com.jetbrains.php.lang.psi.PhpPsiElementFactory
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.pestphp.pest.PestBundle
fun convertTestNameToSentenceCase(
name: String,
shouldLowercaseWords: Boolean = true
) = NameUtilCore.splitNameIntoWordList(name).fold("") { acc, element ->
val word = if (shouldLowercaseWords) element.replaceFirstChar(Char::lowercase) else element
if (acc.lastOrNull()?.isLetterOrDigit() != true || word.length == 1 && !word[0].isLetterOrDigit())
"$acc$word"
else
"$acc $word"
}
fun isInvalidNameCase(name: String) = !name.contains(' ') && convertTestNameToSentenceCase(name, false) != name
class ChangeTestNameCasingQuickFix : LocalQuickFix {
override fun getFamilyName(): String {
return PestBundle.message("QUICK_FIX_CHANGE_TEST_NAME_CASING")
}
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val nameParameter = descriptor.psiElement as? StringLiteralExpression ?: return
val pestTestName = nameParameter.contents
val newNameParameter = PhpPsiElementFactory.createStringLiteralExpression(
project,
convertTestNameToSentenceCase(pestTestName),
true
)
nameParameter.replace(newNameParameter)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/inspections/InvalidTestNameCaseInspection.kt
================================================
package com.pestphp.pest.inspections
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.php.lang.inspections.PhpInspection
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.pestphp.pest.PestBundle
import com.pestphp.pest.getInitialFunctionReference
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.getPestTests
class InvalidTestNameCaseInspection : PhpInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpFile(file: PhpFile) {
file.getPestTests()
.groupBy { it.getPestTestName() }
.filterKeys { it != null }
.filterKeys {
// Remove `it ` prefix from test names
val testName = if (it!!.startsWith("it ")) it.substring(3) else it
isInvalidNameCase(testName)
}
.forEach {
declareProblemType(holder, it.value)
}
}
}
}
private fun declareProblemType(holder: ProblemsHolder, tests: List) {
tests
.mapNotNull { it.getInitialFunctionReference()?.getParameter(0) }
.forEach {
holder.registerProblem(
it,
PestBundle.message("INSPECTION_INVALID_TEST_NAME_CASE"),
ChangeTestNameCasingQuickFix()
)
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/inspections/MissingScreenshotSnapshotInspection.kt
================================================
package com.pestphp.pest.inspections
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.findParentOfType
import com.jetbrains.php.lang.PhpLangUtil
import com.jetbrains.php.lang.inspections.PhpInspection
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.pestphp.pest.PestBundle
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.isPestTestFile
class MissingScreenshotSnapshotInspection : PhpInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpMethodReference(reference: MethodReference) {
val methodName = reference.name ?: return
if (!PhpLangUtil.equalsMethodNames(methodName, "assertScreenshotMatches")) return
if (!reference.containingFile.isPestTestFile()) return
val pestCall = reference.findParentOfType() ?: return
val testName = pestCall.getPestTestName() ?: return
if (!snapshotExists(reference, testName)) {
val namePsi = reference.nameNode?.psi ?: reference
holder.registerProblem(
namePsi,
PestBundle.message("INSPECTION_MISSING_SCREENSHOT_SNAPSHOT")
)
}
}
}
}
private fun snapshotExists(context: MethodReference, testName: String): Boolean {
val file = context.containingFile.originalFile.virtualFile ?: return false
val testsRoot = getTestsRoot(file) ?: return false
val relativePath = VfsUtil.getRelativePath(file.parent, testsRoot) ?: return false
val snapshotPath = ".pest/snapshots/$relativePath/${file.nameWithoutExtension}"
val expectedDir = testsRoot.findFileByRelativePath(snapshotPath) ?: return false
val normalizedTestName = testName.replace("_", "__").replace(Regex("[^a-zA-Z0-9→]"), "_")
return expectedDir.children?.any { it.name.matches("${normalizedTestName}(__\\d+)?\\.snap".toRegex()) } == true
}
private fun getTestsRoot(file: VirtualFile): VirtualFile? {
var currentDir = file.parent
while (currentDir != null && currentDir.name != "tests") {
currentDir = currentDir.parent
}
return currentDir
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/inspections/MultipleExpectChainableInspection.kt
================================================
package com.pestphp.pest.inspections
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.php.lang.inspections.PhpInspection
import com.jetbrains.php.lang.psi.elements.GroupStatement
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.Statement
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.pestphp.pest.PestBundle
import com.pestphp.pest.features.customExpectations.isExpectation
class MultipleExpectChainableInspection : PhpInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpGroupStatement(groupStatement: GroupStatement) {
var counter = 1
groupStatement.statements
.filterIsInstance(Statement::class.java)
.groupBy {
val methodReference = it.firstPsiChild as? MethodReference
if (methodReference?.text?.startsWith("expect") != true || !methodReference.type.isExpectation(holder.project)) {
counter++
return@groupBy 0
}
counter
}
.toMutableMap()
// Drop index 0, as that is all non expect calls
.also { it.remove(0) }
// Filter all expect call groups with only one expect call
.filterValues { it.size >= 2 }
.forEach {
declareProblemType(holder, it.value)
}
}
}
}
@Suppress("SpreadOperator")
private fun declareProblemType(holder: ProblemsHolder, statements: List) {
statements
.forEach {
holder.registerProblem(
it,
PestBundle.message("INSPECTION_MULTIPLE_CHAINABLE_EXPECT_CALLS"),
ChangeMultipleExpectCallsToChainableQuickFix()
)
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/inspections/PestAssertionCanBeSimplifiedInspection.kt
================================================
package com.pestphp.pest.inspections
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.modcommand.ActionContext
import com.intellij.modcommand.ModPsiUpdater
import com.intellij.modcommand.PsiUpdateModCommandAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.text.StringUtil.capitalize
import com.intellij.openapi.util.text.StringUtil.toLowerCase
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.SmartPointerManager
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import com.jetbrains.php.lang.PhpLangUtil
import com.jetbrains.php.lang.inspections.PhpInspection
import com.jetbrains.php.lang.inspections.probablyBug.PhpDivisionByZeroInspection
import com.jetbrains.php.lang.lexer.PhpTokenTypes
import com.jetbrains.php.lang.parser.PhpElementTypes
import com.jetbrains.php.lang.psi.PhpPsiElementFactory
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.PhpTypedElement
import com.jetbrains.php.lang.psi.elements.impl.ParameterListImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.pestphp.pest.PestBundle
internal class PestAssertionCanBeSimplifiedInspection : PhpInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpMethodReference(reference: MethodReference) {
val methodNamePsi = reference.nameNode?.psi ?: return
getMainParameterFromToBe(reference, methodNamePsi)?.let { mainParameter ->
registerProblem(methodNamePsi, mainParameter, "toBe${capitalize(toLowerCase(mainParameter.text))}")
}
getMainParameterFromToHaveCount(reference, methodNamePsi)?.let { mainParameter ->
registerProblem(methodNamePsi, mainParameter, "toBeEmpty")
}
}
private fun getMainParameterFromToBe(reference: MethodReference, methodNameIdentifier: PsiElement): PsiElement? {
val parameter = reference.parameterList?.getParameter("count", 0) ?: return null
if (PhpLangUtil.equalsMethodNames(methodNameIdentifier.text, "toBe") &&
(PhpLangUtil.isTrue(parameter) || PhpLangUtil.isFalse(parameter) || PhpLangUtil.isNull(parameter))) {
return parameter
}
return null
}
private fun getMainParameterFromToHaveCount(reference: MethodReference, methodNameIdentifier: PsiElement): PsiElement? {
val parameter = reference.parameterList?.getParameter("expected", 0) ?: return null
if (PhpLangUtil.equalsMethodNames(methodNameIdentifier.text, "toHaveCount") &&
PhpPsiUtil.isOfType(parameter, PhpElementTypes.NUMBER) &&
PhpDivisionByZeroInspection.isZero(parameter)) {
val functionCall = reference.classReference as? FunctionReference
if (functionCall == null || functionCall.parameters.size != 1) return null
val functionName = functionCall.name
val functionParameter = functionCall.parameters.first()
if (functionName == "expect" && functionParameter is PhpTypedElement && PhpType.isArray(functionParameter.globalType)) {
return parameter
}
}
return null
}
private fun registerProblem(methodNamePsi: PsiElement,
parameterToRemove: PsiElement,
newMethodName: String) {
holder.problem(methodNamePsi, PestBundle.message("INSPECTION_ASSERTION_CAN_BE_SIMPLIFIED", methodNamePsi.text, newMethodName))
.fix(PestSimplifyAssertionQuickFix(newMethodName, methodNamePsi, parameterToRemove))
.register()
}
}
}
private class PestSimplifyAssertionQuickFix(
private val newMethodName: String,
methodNamePsi: PsiElement,
parameterToRemove: PsiElement
) : PsiUpdateModCommandAction(methodNamePsi) {
private val parameterToRemovePointer = SmartPointerManager.getInstance(parameterToRemove.getProject())
.createSmartPsiElementPointer(parameterToRemove)
override fun getFamilyName() = PestBundle.message("QUICK_FIX_SIMPLIFY_ASSERTION")
override fun invoke(context: ActionContext, methodNamePsi: PsiElement, updater: ModPsiUpdater) {
val parameterToRemove = updater.getWritable(parameterToRemovePointer.element) ?: return
val methodReference = methodNamePsi.parent as? MethodReference
if (methodReference == null) return
val methodEnd = PhpPsiUtil.findNextSiblingOfAnyType(methodNamePsi, PhpTokenTypes.chRPAREN) ?: return
(methodReference.parameterList as? ParameterListImpl)?.removeParameter(parameterToRemove)
val newMethodCallText = "$newMethodName(${methodReference.parameterList?.text})"
val newMethodReference = insertIntoMethodReference(methodReference,
TextRange(methodNamePsi.startOffset, methodEnd.endOffset),
newMethodCallText,
context.project)
methodReference.replace(newMethodReference)
}
private fun insertIntoMethodReference(methodReference: MethodReference,
insertionRange: TextRange,
insertionText: String,
project: Project): MethodReference {
val referenceRange = methodReference.textRange
val referenceText = methodReference.text
val referenceRelativeStart = insertionRange.startOffset - referenceRange.startOffset
val referenceRelativeEnd = insertionRange.endOffset - referenceRange.startOffset
val newMethodReferenceText = referenceText.substring(0, referenceRelativeStart) +
insertionText +
referenceText.substring(referenceRelativeEnd)
return PhpPsiElementFactory.createMethodReference(project, newMethodReferenceText)
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/inspections/PestTestFailedLineInspection.kt
================================================
package com.pestphp.pest.inspections
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.php.lang.inspections.PhpTestFailedLineInspectionBase
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.pestphp.pest.isPestTestReference
import com.pestphp.pest.runner.PestFailedLineManager
class PestTestFailedLineInspection : PhpTestFailedLineInspectionBase() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpFunctionCall(functionCall: FunctionReference) {
if (!functionCall.isPestTestReference()) return
val failedLineManager = holder.project.getService(PestFailedLineManager::class.java)
process(holder, functionCall, failedLineManager)
}
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/inspections/SuppressExpressionResultUnusedInspection.kt
================================================
package com.pestphp.pest.inspections
import com.intellij.codeInspection.InspectionSuppressor
import com.intellij.codeInspection.SuppressQuickFix
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.inspections.PhpExpressionResultUnusedInspection
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.pestphp.pest.isPestTestFunction
class SuppressExpressionResultUnusedInspection : InspectionSuppressor {
companion object {
private val SUPPRESSED_PHP_INSPECTIONS = listOf(PhpExpressionResultUnusedInspection().id)
}
override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean {
if (element !is FunctionReferenceImpl) {
return false
}
if (!element.isPestTestFunction()) {
return false
}
return SUPPRESSED_PHP_INSPECTIONS.contains(toolId)
}
override fun getSuppressActions(element: PsiElement?, toolId: String): Array {
return SuppressQuickFix.EMPTY_ARRAY
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/inspections/SuppressUndefinedPropertyInspection.kt
================================================
package com.pestphp.pest.inspections
import com.intellij.codeInspection.InspectionSuppressor
import com.intellij.codeInspection.SuppressQuickFix
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.inspections.PhpDynamicFieldDeclarationInspection
import com.jetbrains.php.lang.inspections.PhpUndefinedFieldInspection
import com.jetbrains.php.lang.psi.elements.FieldReference
import com.pestphp.pest.isAnyPestFunction
import com.pestphp.pest.isThisVariableInPest
class SuppressUndefinedPropertyInspection : InspectionSuppressor {
private val suppressedPhpInspections = listOf(PhpUndefinedFieldInspection().id, PhpDynamicFieldDeclarationInspection().id)
override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean {
if (!suppressedPhpInspections.contains(toolId)) {
return false
}
val fieldReference = element.parent as? FieldReference ?: return false
if (!fieldReference.classReference.isThisVariableInPest { it.isAnyPestFunction() }) {
return false
}
return suppressedPhpInspections.contains(toolId)
}
override fun getSuppressActions(element: PsiElement?, toolId: String): Array {
return SuppressQuickFix.EMPTY_ARRAY
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/notifications/OutdatedNotification.kt
================================================
package com.pestphp.pest.notifications
import com.intellij.notification.Notification
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.Nls
class OutdatedNotification {
private val group = NotificationGroupManager.getInstance()
.getNotificationGroup("Outdated Pest")
fun notify(project: Project?, @Nls content: String): Notification {
val notification: Notification = group.createNotification(content, NotificationType.ERROR)
notification.notify(project)
return notification
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFile.kt
================================================
package com.pestphp.pest.parser
import com.jetbrains.php.lang.psi.resolve.types.PhpType
data class PestConfigurationFile(
val baseTestType: PhpType,
val pathsClasses: List>
)
================================================
FILE: src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFileParser.kt
================================================
package com.pestphp.pest.parser
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiRecursiveElementWalkingVisitor
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.PhpPsiElement
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.elements.impl.PhpFilePathUtils
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.pestphp.pest.CONFIGURATION_FUNCTIONS
import com.pestphp.pest.PestSettings
import com.pestphp.pest.features.configuration.getConfigurationFunctionCall
import com.pestphp.pest.getBaseDir
import com.pestphp.pest.getConfigurationPhpType
import com.pestphp.pest.getPestConfigurationPhpType
import kotlin.io.path.Path
class PestConfigurationFileParser(private val settings: PestSettings) {
fun parse(project: Project, virtualFile: VirtualFile? = null): PestConfigurationFile {
val projectDir = project.guessProjectDir() ?: return defaultConfig
// Use the location of the composer.json file or the project dir
val baseDir = getBaseDir(project, virtualFile) ?: return defaultConfig
val pestFile = VirtualFileManager.getInstance().findFileByUrl(baseDir.url + "/" + settings.pestFilePath)
?: return defaultConfig
val psiFile = PsiManager.getInstance(project).findFile(pestFile) as? PhpFile ?: return defaultConfig
return CachedValuesManager.getCachedValue(psiFile, cacheKey) {
var baseType = PhpType().add("\\PHPUnit\\Framework\\TestCase")
val inPaths = mutableListOf>()
val relativePath = Path(projectDir.path).relativize(Path(baseDir.path)).toString().run {
if (this.isBlank()) this else "$this/"
}
val testsPath = relativePath + settings.pestFilePath.replaceAfterLast("/", "", "")
psiFile.acceptChildren(
Visitor { type, inPath, fullPath ->
if (fullPath && inPath != null) {
inPaths.add(Pair(inPath.replaceBefore(testsPath, ""), type))
} else if (inPath != null) {
inPaths.add(Pair(testsPath + inPath, type))
} else {
baseType = type
}
}
)
CachedValueProvider.Result.create(PestConfigurationFile(baseType, inPaths), psiFile)
} ?: defaultConfig
}
private class Visitor(private val collect: (PhpType, String?, Boolean) -> Unit) :
PsiRecursiveElementWalkingVisitor() {
override fun visitElement(element: PsiElement) {
if (element is MethodReference) {
if (element.name == "in") {
visitInReference(element)
} else if (getConfigurationFunctionCall(element)?.name in CONFIGURATION_FUNCTIONS) {
collect(element.getPestConfigurationPhpType() ?: return,
if (getConfigurationFunctionCall(element)?.name == "pest" &&
element.containingFile.name == CONFIGURATION_FILE_NAME) DEFAULT_DIRECTORY else null,
false)
}
return
} else if (element is FunctionReferenceImpl) {
if (element.name == "uses") {
collect(element.getConfigurationPhpType() ?: return, null, false)
}
return
}
super.visitElement(element)
}
private fun visitInReference(inReference: MethodReference) {
var reference = inReference
var usesType: PhpType? = null
while (true) {
val ref = reference.classReference ?: return
if (ref is MethodReference) {
reference = ref
} else if (ref is FunctionReferenceImpl) {
if (ref.name in CONFIGURATION_FUNCTIONS) {
usesType = (inReference.classReference as? FunctionReference)?.getPestConfigurationPhpType()
}
break
} else {
return
}
}
if (usesType == null) return
inReference.parameters
.map {
PhpFilePathUtils.getStaticPath(it as PhpPsiElement?)
}
.forEach {
collect(usesType, it, false)
}
}
}
companion object {
private val defaultConfig = PestConfigurationFile(
PhpType().add("\\PHPUnit\\Framework\\TestCase"),
emptyList()
)
private val cacheKey = Key>("com.pestphp.pest_configuration")
private const val DEFAULT_DIRECTORY = ""
private const val CONFIGURATION_FILE_NAME = "Pest.php"
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/runner/LocationInfo.kt
================================================
package com.pestphp.pest.runner
import com.intellij.openapi.vfs.VirtualFile
class LocationInfo(
val file: VirtualFile,
val testName: String?
)
================================================
FILE: src/main/kotlin/com/pestphp/pest/runner/PestConsoleProperties.kt
================================================
package com.pestphp.pest.runner
import com.intellij.execution.Executor
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.impl.ConsoleViewImpl
import com.intellij.execution.testframework.Printer
import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction
import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties
import com.intellij.execution.testframework.sm.runner.SMTRunnerEventsListener
import com.intellij.execution.testframework.sm.runner.SMTestLocator
import com.intellij.execution.testframework.sm.runner.SMTestProxy
import com.intellij.execution.testframework.sm.runner.ui.TestStackTraceParser
import com.intellij.execution.ui.ConsoleView
import com.intellij.openapi.project.Project
import com.pestphp.pest.PestBundle
import com.pestphp.pest.configuration.PestLocationProvider
import com.pestphp.pest.configuration.PestRerunFailedTestsAction
import com.pestphp.pest.features.parallel.PestParallelSMTEventsAdapter
import com.pestphp.pest.features.parallel.PestParallelTestExecutor
import com.pestphp.pest.features.parallel.executeInParallel
class PestConsoleProperties(
config: RunConfiguration,
executor: Executor,
private val testLocator: PestLocationProvider
) : SMTRunnerConsoleProperties(config, PestBundle.message("FRAMEWORK_NAME"), executor) {
init {
if (executor is PestParallelTestExecutor || executeInParallel(config)) {
config.project.messageBus.connect(this)
.subscribe(SMTRunnerEventsListener.TEST_STATUS, PestParallelSMTEventsAdapter())
}
}
override fun getTestLocator(): SMTestLocator {
return testLocator
}
override fun createRerunFailedTestsAction(consoleView: ConsoleView?): AbstractRerunFailedTestsAction? {
return consoleView?.let { PestRerunFailedTestsAction(it, this) }
}
override fun isPrintTestingStartedTime(): Boolean {
return false
}
override fun printExpectedActualHeader(printer: Printer, expected: String, actual: String) {
super.printExpectedActualHeader(printer, expected, actual)
}
override fun createConsole(): ConsoleView {
return super.createConsole() as ConsoleViewImpl
}
override fun getTestStackTraceParser(url: String, proxy: SMTestProxy, project: Project): TestStackTraceParser {
return parse(url, proxy.stacktrace, proxy.errorMessage, testLocator, project)
}
@Deprecated("Deprecated in Java", ReplaceWith("true"))
override fun serviceMessageHasNewLinePrefix(): Boolean {
return true
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/runner/PestFailedLineManager.kt
================================================
package com.pestphp.pest.runner
import com.intellij.execution.TestStateStorage
import com.intellij.openapi.components.Service
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.phpunit.PhpUnitTestRunLineMarkerProvider
import com.jetbrains.php.phpunit.PhpUnitTestRunLineMarkerProvider.createPathMapper
import com.jetbrains.php.testFramework.PhpTestFrameworkFailedLineManager
import com.pestphp.pest.configuration.PestLocationProvider
import com.pestphp.pest.features.datasets.isDatasetCall
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.withoutFirstFileSeparator
@Service(Service.Level.PROJECT)
class PestFailedLineManager(
project: Project
) : PhpTestFrameworkFailedLineManager(project), FileEditorManagerListener {
override fun getTestLocationUrl(testElement: PsiElement): String? {
if (testElement !is FunctionReference) return null
return getLocationUrl(testElement.containingFile, testElement)
}
override fun getRecordsForTest(testElement: PsiElement): List {
val testLocationUrl = getTestLocationUrl(testElement) ?: return emptyList()
val testStateRecord = TestStateStorage.getInstance(testElement.project).getState(testLocationUrl) ?: return emptyList()
val project = testElement.getProject()
val records = mutableListOf(testStateRecord)
if (testStateRecord.failedLine == -1 && (testElement.parent as? MethodReference)?.isDatasetCall() == true) {
val allRecordLocationUrls = TestStateStorage.getInstance(project).keys
val dataSetRecords: List = allRecordLocationUrls
.asSequence()
.filterNotNull()
.filter { recordLocationUrl -> isLocationUrlWithNamedDatasetValue(recordLocationUrl, testLocationUrl) }
.map { recordLocationUrl -> TestStateStorage.getInstance(project).getState(recordLocationUrl) }
.filterNotNull()
.filter { record -> record.failedLine != -1 }
.toList()
records.addAll(dataSetRecords)
}
return records
}
private fun isLocationUrlWithNamedDatasetValue(recordLocationUrl: String, testLocationUrl: String): Boolean =
recordLocationUrl.startsWith("$testLocationUrl with data set \"dataset")
private fun getLocationUrl(containingFile: PsiFile, functionCall: FunctionReference): String =
getLocationUrl(containingFile) + "::" + functionCall.getPestTestName()
}
internal fun getLocationUrl(psiFile: PsiFile): String {
return "${PestLocationProvider.PROTOCOL_ID}://${
PhpUnitTestRunLineMarkerProvider.getFilePathDeploymentAware(psiFile)
.removePrefix(getProjectPathDeploymentAware(psiFile.project)).withoutFirstFileSeparator
}"
}
private fun getProjectPathDeploymentAware(project: Project): String {
val projectPath = project.basePath ?: return ""
val remoteMapper = createPathMapper(project)
return if (remoteMapper.canProcess(projectPath)) {
remoteMapper.process(projectPath)
} else {
projectPath
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/runner/PestPressToContinueAction.kt
================================================
package com.pestphp.pest.runner
import com.intellij.execution.impl.ConsoleViewImpl
import com.intellij.execution.testframework.ui.BaseTestsOutputConsoleView
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.util.TextRange
import com.pestphp.pest.PestBundle
import com.pestphp.pest.configuration.PestRunConfigurationType
import java.io.IOException
import java.nio.charset.StandardCharsets
class PestPressToContinueAction : DumbAwareAction() {
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
override fun update(e: AnActionEvent) {
val descriptor = e.getData(LangDataKeys.RUN_CONTENT_DESCRIPTOR)
val processHandler = descriptor?.processHandler
e.presentation.setText(PestBundle.messagePointer("action.press.to.continue.text"))
e.presentation.isVisible = descriptor?.runConfigurationTypeId == PestRunConfigurationType.instance.id
e.presentation.isEnabled = processHandler != null &&
!processHandler.isProcessTerminated &&
!processHandler.isProcessTerminating &&
getInnerConsoleViewImpl(descriptor)?.let { shouldEnableAndPrintHint(it) } == true
}
override fun actionPerformed(e: AnActionEvent) {
val descriptor = e.getData(LangDataKeys.RUN_CONTENT_DESCRIPTOR) ?: return
val processHandler = descriptor.processHandler ?: return
val processInput = processHandler.processInput ?: return
processInput.let { stream ->
try {
stream.write("\n".toByteArray(StandardCharsets.UTF_8))
stream.flush()
} catch (io: IOException) {
logger.warn("Failed to write to process stdin for Pest Press to continue", io)
}
}
}
private fun getInnerConsoleViewImpl(descriptor: RunContentDescriptor): ConsoleViewImpl? {
val baseConsole = descriptor.executionConsole as? BaseTestsOutputConsoleView
return baseConsole?.console as? ConsoleViewImpl
}
private fun readLastNonEmptyLineOrEmpty(view: ConsoleViewImpl): String {
val editor = view.editor ?: return ""
val doc = editor.document
var line = doc.lineCount - 1
while (line >= 0) {
val start = doc.getLineStartOffset(line)
val end = doc.getLineEndOffset(line)
val text = doc.getText(TextRange(start, end))
if (text.any { !it.isWhitespace() }) {
return text
}
line--
}
return ""
}
private fun shouldEnableAndPrintHint(view: ConsoleViewImpl): Boolean {
val line = readLastNonEmptyLineOrEmpty(view)
if (line.contains(PROMPT)) {
view.print("\n $HINT\n", ConsoleViewContentType.SYSTEM_OUTPUT)
return true
}
return line.contains(HINT)
}
internal companion object {
private val logger = Logger.getInstance(PestPressToContinueAction::class.java)
internal const val PROMPT: String = "Press any key to continue"
internal const val HINT: String = "To continue, click \"Continue Test Run\" on the test results' toolbar."
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/runner/PestPromptConsoleFolding.kt
================================================
package com.pestphp.pest.runner
import com.intellij.execution.ConsoleFolding
import com.intellij.execution.ui.ConsoleView
import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.project.Project
import com.pestphp.pest.configuration.PestRunConfigurationType
class PestPromptConsoleFolding : ConsoleFolding() {
override fun isEnabledForConsole(consoleView: ConsoleView): Boolean {
val context = DataManager.getInstance().getDataContext(consoleView.component)
val descriptor = context.getData(LangDataKeys.RUN_CONTENT_DESCRIPTOR) ?: return false
return descriptor.runConfigurationTypeId == PestRunConfigurationType.instance.id
}
override fun shouldFoldLine(project: Project, line: String): Boolean {
return line.contains(PestPressToContinueAction.PROMPT)
}
override fun getPlaceholderText(project: Project, lines: List): String {
return ""
}
override fun shouldBeAttachedToThePreviousLine(): Boolean {
return false
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/runner/PestTestStackTraceParser.kt
================================================
package com.pestphp.pest.runner
import com.intellij.execution.testframework.sm.runner.ui.TestStackTraceParser
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiManager
import com.intellij.util.DocumentUtil
import com.jetbrains.php.util.pathmapper.PhpLocalPathMapper
import com.pestphp.pest.configuration.PestLocationProvider
fun parse(url: String,
stacktrace: String?,
errorMessage: String?,
locator: PestLocationProvider,
project: Project): PestTestStackTraceParser {
if (stacktrace == null) return PestTestStackTraceParser(errorMessage)
val lines = stacktrace.split("\n")
if (lines.isEmpty()) return PestTestStackTraceParser(errorMessage)
val realErrorMessage = if (errorMessage.isNullOrEmpty()) lines[0] else errorMessage
val path = url.removePrefix("${PestLocationProvider.PROTOCOL_ID}://").substringBefore( "::")
val lastLine = lines.last().trim { it <= ' ' }.substringAfter("at ")
if (path == url || !lastLine.startsWith(path)) return PestTestStackTraceParser(realErrorMessage)
val failedLine = StringUtil.parseInt(lastLine.substring(path.length + 1), -1)
val failedLineText = if (failedLine > 0) getLineText(path, failedLine, project, locator) else null
return PestTestStackTraceParser(failedLine, failedLineText, realErrorMessage, null)
}
private fun getLineText(
path: String,
line: Int,
project: Project,
locator: PestLocationProvider
): String? {
val fileUrl = locator.calculateFileUrl(path)
val vFile = locator.pathMapper.getLocalFile(fileUrl) ?: PhpLocalPathMapper(project).getLocalFile(fileUrl) ?: return null
val psiFile = PsiManager.getInstance(project).findFile(vFile) ?: return null
val document = PsiDocumentManager.getInstance(project).getDocument(psiFile) ?: return null
if (line > document.lineCount) return null
val range = DocumentUtil.getLineTextRange(document, line - 1)
return document.getText(range)
}
class PestTestStackTraceParser(
failedLine: Int,
failedMethodName: String?,
errorMessageName: String?,
topLocationLine: String?,
) : TestStackTraceParser(failedLine, failedMethodName, errorMessageName, topLocationLine) {
constructor(errorMessage: String?) : this(-1, null, errorMessage, null)
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/statistics/PestUsagesCollector.kt
================================================
package com.pestphp.pest.statistics
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.openapi.project.Project
object PestUsagesCollector : CounterUsagesCollector() {
private val GROUP = EventLogGroup("pest", 2)
private val PEST_MUTATION_TEST_EXECUTED = GROUP.registerEvent("pest.mutation.test.executed")
private val PEST_PARALLEL_TEST_EXECUTED = GROUP.registerEvent("pest.parallel.test.executed")
override fun getGroup(): EventLogGroup = GROUP
fun logMutationTestExecution(project: Project) {
PEST_MUTATION_TEST_EXECUTED.log(project)
}
fun logParallelTestExecution(project: Project) {
PEST_PARALLEL_TEST_EXECUTED.log(project)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/structureView/PestStructureViewElement.kt
================================================
package com.pestphp.pest.structureView
import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.ItemPresentation
import com.intellij.psi.NavigatablePsiElement
import com.pestphp.pest.PestIcons
import com.pestphp.pest.getPestTestName
import com.pestphp.pest.isPestTestReference
/**
* Defines how the elements in the structure view
* should be rendered.
*/
class PestStructureViewElement(val element: NavigatablePsiElement) : StructureViewTreeElement {
override fun getPresentation(): ItemPresentation {
if (!element.isPestTestReference()) {
return element.presentation ?: PresentationData()
}
return PresentationData(
element.getPestTestName(),
null,
PestIcons.Logo,
null,
)
}
override fun getChildren(): Array {
return arrayOf()
}
override fun navigate(requestFocus: Boolean) {
return element.navigate(requestFocus)
}
override fun canNavigate(): Boolean {
return element.canNavigate()
}
override fun canNavigateToSource(): Boolean {
return element.canNavigateToSource()
}
override fun getValue(): Any {
return element
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/structureView/PestStructureViewExtension.kt
================================================
package com.pestphp.pest.structureView
import com.intellij.ide.structureView.StructureViewExtension
import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.PhpFile
import com.pestphp.pest.getPestTests
/**
* Extends the structure view, so we can include all
* the pest tests in it.
*/
class PestStructureViewExtension : StructureViewExtension {
override fun getType(): Class {
return PhpFile::class.java
}
override fun getChildren(parent: PsiElement?): Array {
if (parent !is PhpFile) {
return arrayOf()
}
return parent.getPestTests()
.map { PestStructureViewElement(it) }
.toTypedArray()
}
override fun getCurrentEditorElement(editor: Editor?, parent: PsiElement?): Any? {
return null
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/surrounders/ExpectStatementSurrounder.kt
================================================
package com.pestphp.pest.surrounders
import com.intellij.lang.surroundWith.Surrounder
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.PhpPsiElementFactory
import com.pestphp.pest.PestBundle
class ExpectStatementSurrounder : Surrounder {
override fun getTemplateDescription(): String {
return PestBundle.message("EXPECT_STATEMENT")
}
override fun isApplicable(elements: Array): Boolean {
return true
}
override fun surroundElements(project: Project, editor: Editor, elements: Array): TextRange? {
val template = PhpPsiElementFactory.createStatement(
project,
"expect(${elements.joinToString("") { it.text }})"
)
val lastElement = elements.last()
val replaced = lastElement.replace(template)
elements.forEach { it.delete() }
return replaced.textRange
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/surrounders/StatementSurroundDescriptor.kt
================================================
package com.pestphp.pest.surrounders
import com.intellij.lang.surroundWith.SurroundDescriptor
import com.intellij.lang.surroundWith.Surrounder
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.jetbrains.php.surroundWith.PhpStatementSurroundDescriptor
import com.pestphp.pest.getPestTests
class StatementSurroundDescriptor : SurroundDescriptor {
override fun getElementsToSurround(file: PsiFile, startOffset: Int, endOffset: Int): Array {
val range = TextRange(startOffset, endOffset)
val insideTest = file.getPestTests()
.any { it.textRange.contains(range) }
if (!insideTest) {
return arrayOf()
}
return PhpStatementSurroundDescriptor().getElementsToSurround(
file, startOffset, endOffset
)
}
override fun getSurrounders(): Array {
return arrayOf(ExpectStatementSurrounder())
}
override fun isExclusive(): Boolean {
return false
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/templates/PestConfigNewDatasetFileAction.kt
================================================
package com.pestphp.pest.templates
import com.intellij.ide.actions.CreateFileFromTemplateDialog
import com.intellij.ide.fileTemplates.FileTemplate
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiFile
import com.pestphp.pest.PestBundle
import com.pestphp.pest.PestIcons
/**
* Shows the "Create Pest Dataset File" action in the context menu when creating a new file.
*
* This will only show if the file is being created in a directory named "tests".
*/
private class PestConfigNewDatasetFileAction : PestConfigNewFileAction() {
companion object {
const val PEST_SHARED_DATASET_TEMPLATE = "Pest Shared Dataset"
const val PEST_SCOPED_DATASET_TEMPLATE = "Pest Scoped Dataset"
}
override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) {
builder
.setTitle(PestBundle.message("CREATE_NEW_PEST_DATASET_DIALOG_TITLE"))
.addKind(PestBundle.message("CREATE_NEW_PEST_SHARED_DATASET"), PestIcons.Dataset, PEST_SHARED_DATASET_TEMPLATE)
.addKind(PestBundle.message("CREATE_NEW_PEST_SCOPED_DATASET"), PestIcons.Dataset, PEST_SCOPED_DATASET_TEMPLATE)
}
override fun getActionName(directory: PsiDirectory?, newName: String, templateName: String?): String {
return PestBundle.message("action.Pest.New.Dataset.text")
}
override fun createFileFromTemplate(name: String?, template: FileTemplate, dir: PsiDirectory): PsiFile {
if (template.name == PEST_SHARED_DATASET_TEMPLATE) {
// find parent directory named "tests"
var parentDir = dir
while (parentDir.name != "tests") {
parentDir = parentDir.parentDirectory ?: break
}
val datasetDir = parentDir.findSubdirectory("Datasets")
?: parentDir.createSubdirectory("Datasets")
// Check if first character is lowercase in name
var newName = name
if (name!![0].isLowerCase()) {
newName = name.replaceFirstChar { it.uppercase() }
}
return createFileFromTemplate(
newName,
template,
datasetDir,
defaultTemplateProperty,
true,
mapOf("DATASET_NAME" to name.replaceFirstChar { it.lowercase() })
)!!
}
return createFileFromTemplate(
"Datasets",
template,
dir,
defaultTemplateProperty,
true,
mapOf("DATASET_NAME" to name!!.replaceFirstChar { it.lowercase() })
)!!
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/templates/PestConfigNewFileAction.kt
================================================
package com.pestphp.pest.templates
import com.intellij.ide.actions.CreateFileFromTemplateAction
import com.intellij.ide.actions.CreateFileFromTemplateDialog
import com.intellij.ide.fileTemplates.FileTemplate
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiFile
import com.pestphp.pest.PestBundle
import com.pestphp.pest.PestIcons
import com.pestphp.pest.PestSettings
import com.pestphp.pest.PestSettings.TestFlavor
/**
* Shows the "Create Pest Test File" action in the context menu when creating a new file.
*
* This will only show if the file is being created in a directory named "tests".
*/
open class PestConfigNewFileAction : CreateFileFromTemplateAction() {
companion object {
const val PEST_IT_TEMPLATE = "Pest It"
const val PEST_TEST_TEMPLATE = "Pest Test"
}
override fun isAvailable(dataContext: DataContext): Boolean {
val view = LangDataKeys.IDE_VIEW.getData(dataContext)
var psiDir: PsiDirectory? = null
if (view != null) {
val directories = view.directories
if (directories.size == 1) {
psiDir = directories[0]
}
}
if (psiDir == null || !psiDir.isValid) {
return false
}
val virtualDir = psiDir.virtualFile
if (!virtualDir.isValid || !virtualDir.isDirectory) {
return false
}
return virtualDir.path.contains("tests")
}
override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) {
builder
.setTitle(PestBundle.message("CREATE_NEW_PEST_TEST_DIALOG_TITLE"))
.addKind(PestBundle.message("CREATE_NEW_PEST_IT_FLAVOR"), PestIcons.File, PEST_IT_TEMPLATE)
.addKind(PestBundle.message("CREATE_NEW_PEST_TEST_FLAVOR"), PestIcons.File, PEST_TEST_TEMPLATE)
}
override fun getActionName(directory: PsiDirectory?, newName: String, templateName: String?): String {
return PestBundle.message("action.Pest.New.File.text")
}
override fun createFileFromTemplate(name: String?, template: FileTemplate, dir: PsiDirectory): PsiFile {
PestSettings.getInstance(dir.project).preferredTestFlavor = if (template.name == PEST_IT_TEMPLATE) TestFlavor.IT
else TestFlavor.TEST
var testName = name
if (!name!!.endsWith("test", true)) {
testName = "${name}Test"
}
return super.createFileFromTemplate(testName, template, dir)
}
override fun getDefaultTemplateName(dir: PsiDirectory): String {
return if (PestSettings.getInstance(dir.project).preferredTestFlavor == TestFlavor.IT) {
PEST_IT_TEMPLATE
} else {
PEST_TEST_TEMPLATE
}
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/templates/PestDescribePostfixTemplate.kt
================================================
package com.pestphp.pest.templates
import com.intellij.openapi.editor.Document
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.postfixCompletion.PhpPostfixUtils
import com.jetbrains.php.postfixCompletion.PhpStringBasedPostfixTemplate
import com.pestphp.pest.isPestTestFile
class PestDescribePostfixTemplate : PhpStringBasedPostfixTemplate(
"describe",
"describe($EXPR, function...)",
PhpPostfixUtils.selectorTopmost()
) {
override fun isApplicable(context: PsiElement, copyDocument: Document, newOffset: Int): Boolean {
return context.parent is StringLiteralExpression && context.containingFile.isPestTestFile()
}
override fun getTemplateString(element: PsiElement): String {
val dollar = "$"
return """
describe(${dollar}$EXPR${dollar}, function() {
${dollar}END${dollar}
});
""".trimIndent()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/templates/PestItPostfixTemplate.kt
================================================
package com.pestphp.pest.templates
import com.intellij.openapi.editor.Document
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.jetbrains.php.postfixCompletion.PhpPostfixUtils
import com.jetbrains.php.postfixCompletion.PhpStringBasedPostfixTemplate
import com.pestphp.pest.isPestTestFile
/**
* Adds a postfix template for `it` tests.
*/
class PestItPostfixTemplate : PhpStringBasedPostfixTemplate(
"it",
"it(${EXPR}, function...)",
PhpPostfixUtils.selectorTopmost()
) {
override fun isApplicable(context: PsiElement, copyDocument: Document, newOffset: Int): Boolean {
return context.parent is StringLiteralExpression && context.containingFile.isPestTestFile()
}
override fun getTemplateString(element: PsiElement): String {
val dollar = "$";
return """
it(${dollar}${EXPR}${dollar}, function() {
${dollar}END${dollar}
});
""".trimIndent()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/templates/PestPostfixTemplateProvider.kt
================================================
package com.pestphp.pest.templates
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplate
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateProvider
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
/**
* Register postfix templates
*/
class PestPostfixTemplateProvider : PostfixTemplateProvider {
override fun getTemplates(): MutableSet {
return mutableSetOf(PestItPostfixTemplate(), PestDescribePostfixTemplate())
}
override fun isTerminalSymbol(currentChar: Char): Boolean {
return currentChar == '.'
}
override fun preExpand(file: PsiFile, editor: Editor) = Unit
override fun afterExpand(file: PsiFile, editor: Editor) = Unit
override fun preCheck(copyFile: PsiFile, realEditor: Editor, currentOffset: Int): PsiFile {
return copyFile
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/templates/PestRootTemplateContextType.kt
================================================
package com.pestphp.pest.templates
import com.intellij.codeInsight.template.TemplateActionContext
import com.intellij.codeInsight.template.TemplateContextType
import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.elements.PhpNamespace
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression
import com.pestphp.pest.PestBundle
import com.pestphp.pest.isPestTestFile
/**
* Adds a template context to be used in live templates
*
* This Pest root template checks if the context is the root of a
* pest test file.
*/
class PestRootTemplateContextType : TemplateContextType(PestBundle.message("LIVE_TEMPLATE_PEST_ROOT")) {
override fun isInContext(templateActionContext: TemplateActionContext): Boolean {
if (!templateActionContext.file.isPestTestFile()) {
return false
}
val element = templateActionContext.file.findElementAt(templateActionContext.startOffset)
if (element?.parent is StringLiteralExpression) {
return false
}
// Get root element
val root = element?.parent?.parent?.parent?.parent
// Check if in root is namespace or file
return root is PhpFile || root is PhpNamespace
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/types/HigherOrderExtendTypeProvider.kt
================================================
package com.pestphp.pest.types
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.elements.FieldReference
import com.jetbrains.php.lang.psi.elements.MemberReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.PhpNamedElement
import com.jetbrains.php.lang.psi.elements.PhpTypedElement
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4
import com.pestphp.pest.features.customExpectations.expectationType
class HigherOrderExtendTypeProvider : PhpTypeProvider4 {
override fun getKey(): Char {
return '\u0224' // Ȥ
}
override fun getType(psiElement: PsiElement): PhpType? {
if (DumbService.isDumb(psiElement.project)) return null
val reference = psiElement as? MemberReference ?: return null
if (reference !is FieldReference && reference !is MethodReference) return null
val expectCall = getExpectCall(reference) ?: return null
if (expectCall.parameters.isEmpty()) return null
val firstParameterType = (expectCall.parameters[0] as? PhpTypedElement)?.type ?: return null
return PhpType().add(firstParameterType).add(expectationType)
}
private fun getExpectCall(reference: MemberReference, depth: Int = 50): FunctionReferenceImpl? {
if (depth <= 0) return null
return when (val classReference = reference.classReference) {
is FunctionReferenceImpl -> if (classReference.name == "expect") classReference else null
is MemberReference -> getExpectCall(classReference, depth - 1)
else -> null
}
}
override fun complete(s: String, project: Project): PhpType? {
return null
}
override fun getBySignature(s: String, set: Set, i: Int, project: Project): Collection {
return emptyList()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/types/InnerTestTypeProvider.kt
================================================
package com.pestphp.pest.types
import com.intellij.openapi.project.DumbService
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.pestphp.pest.isAnyPestFunction
import com.pestphp.pest.isTestAsThisVariableInPest
/**
* Extend `test()` type inside a test closure with types from `uses`.
* Both `uses` from the same file, the pest config file
* and `uses` with paths from pest config file.
*/
class InnerTestTypeProvider: ThisTypeProvider() {
override fun getKey(): Char {
return '\u0226' // Ȧ
}
override fun getType(psiElement: PsiElement): PhpType? {
if (DumbService.isDumb(psiElement.project)) return null
if (!psiElement.isTestAsThisVariableInPest { it.isAnyPestFunction() }) return null
return getPestType(psiElement)
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/types/ThisExtendTypeProvider.kt
================================================
package com.pestphp.pest.types
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.elements.PhpNamedElement
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4
import com.pestphp.pest.features.customExpectations.expectationType
import com.pestphp.pest.features.customExpectations.isThisVariableInExtend
/**
* Adds autocompletion for `$this` variable in `expect->extend`.
*/
class ThisExtendTypeProvider : PhpTypeProvider4 {
override fun getKey(): Char {
return '\u0223' // ȣ
}
override fun getType(psiElement: PsiElement): PhpType? {
if (DumbService.isDumb(psiElement.project)) return null
if (!psiElement.isThisVariableInExtend()) return null
return expectationType
}
override fun complete(s: String, project: Project): PhpType? {
return null
}
override fun getBySignature(s: String, set: Set, i: Int, project: Project): Collection {
return emptyList()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/types/ThisFieldTypeProvider.kt
================================================
package com.pestphp.pest.types
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.elements.FieldReference
import com.jetbrains.php.lang.psi.elements.PhpNamedElement
import com.jetbrains.php.lang.psi.elements.PhpTypedElement
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4
import com.pestphp.pest.getAllBeforeThisAssignments
import com.pestphp.pest.isPestAfterFunction
import com.pestphp.pest.isPestTestFunction
import com.pestphp.pest.isThisVariableInPest
/**
* Adds type for fields registered in `beforeEach`.
*/
class ThisFieldTypeProvider : PhpTypeProvider4 {
override fun getKey(): Char {
return '\u0225' // ȥ
}
override fun getType(psiElement: PsiElement): PhpType? {
if (DumbService.isDumb(psiElement.project)) return null
val fieldReference = psiElement as? FieldReference ?: return null
if (!fieldReference.classReference.isThisVariableInPest { check(it) }) return null
val fieldName = fieldReference.name ?: return null
return (psiElement.containingFile ?: return null).getAllBeforeThisAssignments()
.filter { (it.variable as? FieldReference)?.name == fieldName }
.mapNotNull { it.value }
.filterIsInstance()
.firstOrNull()?.type
}
private fun check(it: FunctionReferenceImpl) = it.isPestTestFunction() || it.isPestAfterFunction()
override fun complete(s: String, project: Project): PhpType? {
return null
}
override fun getBySignature(s: String, set: Set, i: Int, project: Project): Collection {
return emptyList()
}
}
================================================
FILE: src/main/kotlin/com/pestphp/pest/types/ThisTypeProvider.kt
================================================
package com.pestphp.pest.types
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.psi.PsiElement
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.PhpNamedElement
import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4
import com.pestphp.pest.PestSettings
import com.pestphp.pest.getPestConfigurationPhpType
import com.pestphp.pest.getRoot
import com.pestphp.pest.isAnyPestFunction
import com.pestphp.pest.isThisVariableInPest
import java.nio.file.FileSystems
import kotlin.io.path.Path
/**
* Extend `$this` type with types from `uses`.
* Both `uses` from the same file, the pest config file
* and `uses` with paths from pest config file.
*/
open class ThisTypeProvider : PhpTypeProvider4 {
override fun getKey(): Char {
return '\u0221' // ȡ
}
override fun getType(psiElement: PsiElement): PhpType? {
if (DumbService.isDumb(psiElement.project)) return null
if (
((psiElement as? FunctionReferenceImpl)?.isAnyPestFunction() != true) &&
!psiElement.isThisVariableInPest { it.isAnyPestFunction() }
) return null
return getPestType(psiElement)
}
protected fun getPestType(psiElement: PsiElement): PhpType? {
val virtualFile = psiElement.containingFile?.originalFile?.virtualFile ?: return null
val config = PestSettings.getInstance(psiElement.project).getPestConfiguration(psiElement.project, virtualFile)
val baseDir = (psiElement.project.guessProjectDir() ?: return config.baseTestType)
val relativePath = VfsUtil.getRelativePath(virtualFile, baseDir) ?: return config.baseTestType
val result = PhpType().add(config.baseTestType)
val defaultFileSystem = FileSystems.getDefault()
config.pathsClasses.forEach { (path, type) ->
FileUtil.toCanonicalPath(path)?.let { normalizedPathForMatching ->
if (defaultFileSystem.getPathMatcher("glob:$normalizedPathForMatching**").matches(Path(relativePath))) {
result.add(type)
}
}
}
psiElement.containingFile.getRoot()
.filterIsInstance()
.mapNotNull { it.getPestConfigurationPhpType() }
.forEach { result.add(it) }
return result
}
override fun complete(s: String, project: Project): PhpType? {
return null
}
override fun getBySignature(s: String, set: Set, i: Int, project: Project): Collection {
return emptyList()
}
}
================================================
FILE: src/main/resources/META-INF/plugin.xml
================================================
com.pestphp.pest-intellij
Pest
JetBrains
Plugin provides Pest PHP test framework support
messages.pestBundle
Test Tools
================================================
FILE: src/main/resources/fileTemplates/internal/Pest It.php.ft
================================================
Reports invalid names for pest dataset cases.