diff --git a/src/main/java/de/espend/idea/php/annotation/AnnotationStubIndex.java b/src/main/java/de/espend/idea/php/annotation/AnnotationStubIndex.java index fc5a2014..63314219 100644 --- a/src/main/java/de/espend/idea/php/annotation/AnnotationStubIndex.java +++ b/src/main/java/de/espend/idea/php/annotation/AnnotationStubIndex.java @@ -5,7 +5,6 @@ import com.intellij.util.io.DataExternalizer; import com.intellij.util.io.EnumeratorStringDescriptor; import com.intellij.util.io.KeyDescriptor; -import com.intellij.util.io.VoidDataExternalizer; import com.jetbrains.php.lang.PhpFileType; import com.jetbrains.php.lang.psi.PhpFile; import com.jetbrains.php.lang.psi.elements.PhpClass; @@ -19,21 +18,21 @@ /** * @author Daniel Espendiller */ -public class AnnotationStubIndex extends FileBasedIndexExtension { - public static final ID KEY = ID.create("espend.php.annotation.classes"); +public class AnnotationStubIndex extends FileBasedIndexExtension { + public static final ID KEY = ID.create("espend.php.annotation.classes"); private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); @NotNull @Override - public ID getName() { + public ID getName() { return KEY; } @NotNull @Override - public DataIndexer getIndexer() { + public DataIndexer getIndexer() { return inputData -> { - final Map map = new HashMap<>(); + final Map map = new HashMap<>(); PsiFile psiFile = inputData.getPsiFile(); if (!(psiFile instanceof PhpFile)) { @@ -54,8 +53,11 @@ public DataIndexer getIndexer() { // doctrine has many tests: Doctrine\Tests\Common\Annotations\Fixtures // we are on index process, project is not fully loaded here, so filter name based tests // e.g. PhpUnitUtil.isTestClass not possible - if (!fqn.contains("\\Tests\\") && !fqn.contains("\\Fixtures\\") && AnnotationUtil.isAnnotationClass(phpClass)) { - map.put(fqn, null); + if (!fqn.contains("\\Tests\\") && !fqn.contains("\\Fixtures\\")) { + String serializedTargets = AnnotationUtil.getSerializedAnnotationTargets(phpClass); + if (serializedTargets != null) { + map.put(fqn, serializedTargets); + } } } } @@ -72,8 +74,8 @@ public KeyDescriptor getKeyDescriptor() { @NotNull @Override - public DataExternalizer getValueExternalizer() { - return VoidDataExternalizer.INSTANCE; + public DataExternalizer getValueExternalizer() { + return EnumeratorStringDescriptor.INSTANCE; } @NotNull @@ -89,6 +91,6 @@ public boolean dependsOnFileContent() { @Override public int getVersion() { - return 2; + return 3; } } diff --git a/src/main/java/de/espend/idea/php/annotation/completion/AnnotationCompletionContributor.java b/src/main/java/de/espend/idea/php/annotation/completion/AnnotationCompletionContributor.java index f76daca8..d1eb9788 100644 --- a/src/main/java/de/espend/idea/php/annotation/completion/AnnotationCompletionContributor.java +++ b/src/main/java/de/espend/idea/php/annotation/completion/AnnotationCompletionContributor.java @@ -3,10 +3,14 @@ import com.intellij.codeInsight.completion.*; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; import com.intellij.patterns.ElementPattern; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiWhiteSpace; +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.intellij.util.ProcessingContext; import com.intellij.util.indexing.FileBasedIndex; @@ -46,6 +50,7 @@ * @author Daniel Espendiller */ public class AnnotationCompletionContributor extends CompletionContributor { + private static final Key>>> ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE = new Key<>("ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE"); private static final ElementPattern DOC_IDENTIFIER_PATTERN = PlatformPatterns.psiElement(PhpDocTokenTypes.DOC_IDENTIFIER); @@ -411,36 +416,73 @@ private void attachLookupElements(@NotNull Project project, @NotNull PhpAttribut items.putAll(getUseAsMap(phpAttributesList)); - for (String fqnClass: FileBasedIndex.getInstance().getAllKeys(PhpAttributesFQNsIndex.KEY, project)) { - if(!fqnClass.startsWith("\\")) { - fqnClass = "\\" + fqnClass; + Map> attributesByNamespace = getAttributeFqnsByNamespace(project); + + for (Map.Entry aliasFqn : items.entrySet()) { + String namespace = "\\" + StringUtils.stripStart(aliasFqn.getValue(), "\\"); + Collection fqns = attributesByNamespace.get(namespace); + if (fqns == null) { + continue; } - // attach class also "@ORM\Entity" if there is not import but an alias via settings - for (Map.Entry aliasFqn : items.entrySet()) { - String className = "\\" + StringUtils.stripStart(aliasFqn.getValue(), "\\") + "\\"; + for (String fqnClass : fqns) { + String substring = fqnClass.substring(namespace.length() + 1); + String lookupString = aliasFqn.getKey() + "\\" + substring; - if (!fqnClass.startsWith(className)) { + PhpClass underlyingClass = PhpElementsUtil.getClassInterface(project, fqnClass); + if (underlyingClass == null) { continue; } - String substring = fqnClass.substring(className.length()); + // check if Attribute is target allowed for context + // @see com.jetbrains.php.completion.PhpCompletionContributor.PhpClassRefCompletionProvider.shouldAddElement + List rootAttributes = PhpClassCantBeUsedAsAttributeInspection.rootAttributes(underlyingClass).collect(Collectors.toList()); + if (!rootAttributes.isEmpty() && PhpInapplicableAttributeTargetDeclarationInspection.getInapplicableDeclarationName(phpAttributesList.getParent(), rootAttributes) == null) { + PhpClassAnnotationLookupElement phpClassAnnotationLookupElement = new PhpClassAnnotationLookupElement(underlyingClass, new UseAliasOption(aliasFqn.getValue(), aliasFqn.getKey(), true), lookupString); + phpClassAnnotationLookupElement.withInsertHandler(AttributeAliasInsertHandler.getInstance()); + completionResultSet.addElement(underlyingClass.isDeprecated() ? PrioritizedLookupElement.withPriority(phpClassAnnotationLookupElement, -1000) : phpClassAnnotationLookupElement); + } + } + } + } - String lookupString = aliasFqn.getKey() + "\\" + substring; + /** + * Cache attribute FQNs by namespace prefix so alias completion can query only relevant branches. + * + * Example key: "\Doctrine\ORM\Mapping" => ["\Doctrine\ORM\Mapping\Entity", ...] + */ + @NotNull + private static Map> getAttributeFqnsByNamespace(@NotNull Project project) { + return CachedValuesManager.getManager(project).getCachedValue( + project, + ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE, + () -> { + Map> items = new HashMap<>(); + + for (String fqnClass : FileBasedIndex.getInstance().getAllKeys(PhpAttributesFQNsIndex.KEY, project)) { + if (!fqnClass.startsWith("\\")) { + fqnClass = "\\" + fqnClass; + } - PhpClass underlyingClass = PhpElementsUtil.getClassInterface(project, fqnClass); - if (underlyingClass != null) { - // check if Attribute is target allowed for context - // @see com.jetbrains.php.completion.PhpCompletionContributor.PhpClassRefCompletionProvider.shouldAddElement - List rootAttributes = PhpClassCantBeUsedAsAttributeInspection.rootAttributes(underlyingClass).collect(Collectors.toList()); - if (!rootAttributes.isEmpty() && PhpInapplicableAttributeTargetDeclarationInspection.getInapplicableDeclarationName(phpAttributesList.getParent(), rootAttributes) == null) { - PhpClassAnnotationLookupElement phpClassAnnotationLookupElement = new PhpClassAnnotationLookupElement(underlyingClass, new UseAliasOption(aliasFqn.getValue(), aliasFqn.getKey(), true), lookupString); - phpClassAnnotationLookupElement.withInsertHandler(AttributeAliasInsertHandler.getInstance()); - completionResultSet.addElement(underlyingClass.isDeprecated() ? PrioritizedLookupElement.withPriority(phpClassAnnotationLookupElement, -1000) : phpClassAnnotationLookupElement); + int index = fqnClass.indexOf("\\", 1); + while (index > 0) { + items.computeIfAbsent(fqnClass.substring(0, index), key -> new ArrayList<>()).add(fqnClass); + index = fqnClass.indexOf("\\", index + 1); } } - } - } + + Map> immutableItems = new HashMap<>(); + for (Map.Entry> entry : items.entrySet()) { + immutableItems.put(entry.getKey(), List.copyOf(entry.getValue())); + } + + return CachedValueProvider.Result.create( + Collections.unmodifiableMap(immutableItems), + AnnotationUtil.getModificationTrackerForIndexId(project, PhpAttributesFQNsIndex.KEY) + ); + }, + false + ); } private static Map getUseAsMap(@NotNull PsiElement phpDocComment) { diff --git a/src/main/java/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java b/src/main/java/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java index 6b13e989..93d059a9 100644 --- a/src/main/java/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java +++ b/src/main/java/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java @@ -51,7 +51,7 @@ public void collectSlowLineMarkers(@NotNull List psiElemen || !phpClass.getAttributes("\\Attribute").isEmpty(); if (!isAnnotationOrAttribute) { - return; + continue; } String fqn = StringUtils.stripStart(phpClass.getFQN(), "\\"); @@ -92,4 +92,4 @@ public void collectSlowLineMarkers(@NotNull List psiElemen private static PsiElementPattern.Capture getClassNamePattern() { return CLASS_NAME_PATTERN; } -} \ No newline at end of file +} diff --git a/src/main/java/de/espend/idea/php/annotation/util/AnnotationUtil.java b/src/main/java/de/espend/idea/php/annotation/util/AnnotationUtil.java index 62ad1236..555b8be7 100644 --- a/src/main/java/de/espend/idea/php/annotation/util/AnnotationUtil.java +++ b/src/main/java/de/espend/idea/php/annotation/util/AnnotationUtil.java @@ -5,12 +5,17 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.ModificationTracker; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.patterns.PlatformPatterns; import com.intellij.patterns.PsiElementPattern; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; +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.intellij.util.Processor; import com.intellij.util.TripleFunction; @@ -54,6 +59,8 @@ * @author Daniel Espendiller */ public class AnnotationUtil { + private static final Key>> ANNOTATION_FQN_MAP_CACHE = new Key<>("ANNOTATION_FQN_MAP_CACHE"); + private static final Pattern ANNOTATION_TARGET_PATTERN = Pattern.compile("\"(\\w+)\""); public static final ExtensionPointName EXTENSION_POINT_COMPLETION = new ExtensionPointName<>("de.espend.idea.php.annotation.PhpAnnotationCompletionProvider"); public static final ExtensionPointName EXTENSION_POINT_REFERENCES = new ExtensionPointName<>("de.espend.idea.php.annotation.PhpAnnotationReferenceProvider"); @@ -100,30 +107,15 @@ public class AnnotationUtil { }}; public static boolean isAnnotationClass(@NotNull PhpClass phpClass) { - PhpDocComment phpDocComment = phpClass.getDocComment(); - if(phpDocComment != null) { - PhpDocTag[] annotationDocTags = phpDocComment.getTagElementsByName("@Annotation"); - return annotationDocTags.length > 0; - } - - return false; + return getClassAnnotationTargets(phpClass) != null; } public static PhpClass[] getAnnotationsClasses(Project project) { - ArrayList phpClasses = new ArrayList<>(); - - CollectProjectUniqueKeys ymlProjectProcessor = new CollectProjectUniqueKeys(project, AnnotationStubIndex.KEY); - FileBasedIndex.getInstance().processAllKeys(AnnotationStubIndex.KEY, ymlProjectProcessor, project); - - for(String phpClassName: ymlProjectProcessor.getResult()) { - PhpClass phpClass = PhpElementsUtil.getClass(project, phpClassName); - if(phpClass != null) { - phpClasses.add(phpClass); - } - - } - - return phpClasses.toArray(new PhpClass[0]); + return getCachedAnnotationsMap(project) + .values() + .stream() + .map(PhpAnnotation::getPhpClass) + .toArray(PhpClass[]::new); } /** @@ -132,51 +124,16 @@ public static PhpClass[] getAnnotationsClasses(Project project) { */ @Nullable public static PhpAnnotation getClassAnnotation(@NotNull PhpClass phpClass) { - if(!AnnotationUtil.isAnnotationClass(phpClass)) { - return null; - } - - PhpDocComment phpDocComment = phpClass.getDocComment(); - if(phpDocComment == null) { - return null; - } - - List targets = new ArrayList<>(); - - PhpDocTag[] tagElementsByName = phpDocComment.getTagElementsByName("@Target"); - - if(tagElementsByName.length > 0) { - for (PhpDocTag phpDocTag : tagElementsByName) { - // @Target("PROPERTY", "METHOD") - // @Target("CLASS") - // @Target("ALL") - String text = phpDocTag.getText(); - Matcher matcher = Pattern.compile("\"(\\w+)\"").matcher(text); - - // regex matched; on invalid we at target to UNKNOWN condition - boolean isMatched = false; - - // match enum value - while (matcher.find()) { - isMatched = true; - try { - targets.add(AnnotationTarget.valueOf(matcher.group(1).toUpperCase())); - } catch (IllegalArgumentException e) { - targets.add(AnnotationTarget.UNKNOWN); - } - } - - // regex failed provide UNKNOWN target - if(!isMatched) { - targets.add(AnnotationTarget.UNKNOWN); - } + String fqn = StringUtils.stripStart(phpClass.getFQN(), "\\"); + if (StringUtils.isNotBlank(fqn)) { + PhpAnnotation cached = getCachedAnnotationsMap(phpClass.getProject()).get(fqn); + if (cached != null) { + return cached; } - } else { - // no target attribute so UNDEFINED target - targets.add(AnnotationTarget.UNDEFINED); } - if(targets.isEmpty()) { + List targets = getClassAnnotationTargets(phpClass); + if (targets == null || targets.isEmpty()) { return null; } @@ -185,24 +142,57 @@ public static PhpAnnotation getClassAnnotation(@NotNull PhpClass phpClass) { @NotNull public static Map getAnnotationsOnTargetMap(@NotNull Project project, AnnotationTarget... targets) { - Map phpAnnotations = new HashMap<>(); - for(PhpClass phpClass: AnnotationUtil.getAnnotationsClasses(project)) { - PhpAnnotation phpAnnotation = AnnotationUtil.getClassAnnotation(phpClass); - if(phpAnnotation != null && phpAnnotation.hasTarget(targets)) { - String fqn = phpClass.getFQN(); - if(fqn.startsWith("\\")) { - fqn = fqn.substring(1); - } - + for (Map.Entry entry : getCachedAnnotationsMap(project).entrySet()) { + PhpAnnotation phpAnnotation = entry.getValue(); + if (phpAnnotation.hasTarget(targets)) { + String fqn = entry.getKey(); phpAnnotations.put(fqn, phpAnnotation); } - } return phpAnnotations; + } + /** + * Build a cached map of all indexed annotation classes keyed by normalized FQN. + */ + @NotNull + public static Map getCachedAnnotationsMap(@NotNull Project project) { + return CachedValuesManager.getManager(project).getCachedValue( + project, + ANNOTATION_FQN_MAP_CACHE, + () -> { + Map phpAnnotations = new HashMap<>(); + GlobalSearchScope scope = GlobalSearchScope.allScope(project); + + FileBasedIndex.getInstance().processAllKeys(AnnotationStubIndex.KEY, fqn -> { + if (StringUtils.isBlank(fqn)) { + return true; + } + + PhpClass phpClass = PhpElementsUtil.getClass(project, fqn); + if (phpClass == null) { + return true; + } + + List values = FileBasedIndex.getInstance().getValues(AnnotationStubIndex.KEY, fqn, scope); + if (values.isEmpty()) { + return true; + } + + phpAnnotations.put(fqn, new PhpAnnotation(phpClass, getAnnotationTargetsFromSerializedValue(values.get(0)))); + return true; + }, project); + + return CachedValueProvider.Result.create( + Collections.unmodifiableMap(phpAnnotations), + getModificationTrackerForIndexId(project, AnnotationStubIndex.KEY) + ); + }, + false + ); } /** @@ -697,6 +687,42 @@ public static Collection getImplementationsForAnnotation(@NotNull Pr return psiElements; } + /** + * Serialize parsed annotation targets for storage in the file index. + * + * @return comma-separated enum names or null if the class is not an annotation + */ + @Nullable + public static String getSerializedAnnotationTargets(@NotNull PhpClass phpClass) { + List targets = getClassAnnotationTargets(phpClass); + if (targets == null || targets.isEmpty()) { + return null; + } + + return serializeAnnotationTargets(targets); + } + + /** + * Restore annotation targets from the file index value. + */ + @NotNull + public static List getAnnotationTargetsFromSerializedValue(@NotNull String serializedValue) { + if (StringUtils.isBlank(serializedValue)) { + return Collections.singletonList(AnnotationTarget.UNDEFINED); + } + + List targets = new ArrayList<>(); + for (String value : StringUtils.split(serializedValue, ',')) { + try { + targets.add(AnnotationTarget.valueOf(value)); + } catch (IllegalArgumentException ignored) { + targets.add(AnnotationTarget.UNKNOWN); + } + } + + return targets.isEmpty() ? Collections.singletonList(AnnotationTarget.UNDEFINED) : targets; + } + @NotNull public static Collection getActiveImportsAliasesFromSettings() { Collection useAliasOptions = ApplicationSettings.getUseAliasOptionsWithDefaultFallback(); @@ -750,6 +776,61 @@ public static void visitAttributes(@NotNull PhpClass phpClass, TripleFunction getClassAnnotationTargets(@NotNull PhpClass phpClass) { + PhpDocComment phpDocComment = phpClass.getDocComment(); + if (phpDocComment == null || phpDocComment.getTagElementsByName("@Annotation").length == 0) { + return null; + } + + List targets = new ArrayList<>(); + PhpDocTag[] tagElementsByName = phpDocComment.getTagElementsByName("@Target"); + + if (tagElementsByName.length > 0) { + for (PhpDocTag phpDocTag : tagElementsByName) { + Matcher matcher = ANNOTATION_TARGET_PATTERN.matcher(phpDocTag.getText()); + boolean isMatched = false; + + while (matcher.find()) { + isMatched = true; + try { + targets.add(AnnotationTarget.valueOf(matcher.group(1).toUpperCase())); + } catch (IllegalArgumentException e) { + targets.add(AnnotationTarget.UNKNOWN); + } + } + + if (!isMatched) { + targets.add(AnnotationTarget.UNKNOWN); + } + } + } else { + targets.add(AnnotationTarget.UNDEFINED); + } + + return targets.isEmpty() ? null : targets; + } + + @NotNull + private static String serializeAnnotationTargets(@NotNull List targets) { + return targets.stream().map(Enum::name).collect(Collectors.joining(",")); + } + + /** + * Provide a modification tracker for a concrete file index. + * + * This is used by project-level caches that derive their content from indexed values. + */ + @NotNull + public static ModificationTracker getModificationTrackerForIndexId(@NotNull Project project, @NotNull final ID id) { + return () -> FileBasedIndex.getInstance().getIndexModificationStamp(id, project); + } + /** * matches "@Callback(propertyName="")" @@ -934,5 +1015,3 @@ public static boolean useAttributeForGenerateDoctrineMetadata(@NotNull PsiFile f return PhpLanguageLevel.current(file.getProject()).hasFeature(PhpLanguageFeature.ATTRIBUTES); } } - - diff --git a/src/test/java/de/espend/idea/php/annotation/tests/AnnotationStubIndexTest.java b/src/test/java/de/espend/idea/php/annotation/tests/AnnotationStubIndexTest.java index fd7ba568..95707bd1 100644 --- a/src/test/java/de/espend/idea/php/annotation/tests/AnnotationStubIndexTest.java +++ b/src/test/java/de/espend/idea/php/annotation/tests/AnnotationStubIndexTest.java @@ -1,6 +1,8 @@ package de.espend.idea.php.annotation.tests; import de.espend.idea.php.annotation.AnnotationStubIndex; +import de.espend.idea.php.annotation.dict.AnnotationTarget; +import de.espend.idea.php.annotation.util.AnnotationUtil; /** * @author Daniel Espendiller @@ -10,6 +12,7 @@ public class AnnotationStubIndexTest extends AnnotationLightCodeInsightFixtureTe public void setUp() throws Exception { super.setUp(); myFixture.copyFileToProject("classes.php"); + myFixture.copyFileToProject("classes_targets.php"); } public String getTestDataPath() { @@ -21,4 +24,15 @@ public void testThatAnnotationClassIsInIndex() { assertIndexContains(AnnotationStubIndex.KEY, "My\\Annotations\\Foo\\RouteBar"); assertIndexContains(AnnotationStubIndex.KEY, "My\\Annotations\\Foo\\RouteFoo"); } + + public void testThatAnnotationTargetsAreStoredInIndex() { + assertIndexContainsKeyWithValue(AnnotationStubIndex.KEY, "My\\Annotations\\PropertyOnly", value -> + AnnotationUtil.getAnnotationTargetsFromSerializedValue(value).contains(AnnotationTarget.PROPERTY) + ); + + assertIndexContainsKeyWithValue(AnnotationStubIndex.KEY, "My\\Annotations\\MethodAndAll", value -> { + java.util.List targets = AnnotationUtil.getAnnotationTargetsFromSerializedValue(value); + return targets.contains(AnnotationTarget.METHOD) && targets.contains(AnnotationTarget.ALL); + }); + } } diff --git a/src/test/java/de/espend/idea/php/annotation/tests/completion/AnnotationCompletionContributorTest.java b/src/test/java/de/espend/idea/php/annotation/tests/completion/AnnotationCompletionContributorTest.java index 9def05a5..bc23df62 100644 --- a/src/test/java/de/espend/idea/php/annotation/tests/completion/AnnotationCompletionContributorTest.java +++ b/src/test/java/de/espend/idea/php/annotation/tests/completion/AnnotationCompletionContributorTest.java @@ -454,6 +454,31 @@ public void testTheInternalAliasProvideCompletionAndImportsForAttributes() { " function foo() {}\n" + " }\n" + "}", + " "ORM\\Entity".equals(lookupElement.getLookupString()) + ); + } + + public void testTheImportedAliasProvideCompletionForAttributes() { + assertCompletionResultEquals(PhpFileType.INSTANCE, "]\n" + + " function foo() {}\n" + + " }\n" + + "}", " propertyAnnotations = AnnotationUtil.getAnnotationsOnTargetMap(getProject(), AnnotationTarget.PROPERTY); + assertContainsElements(propertyAnnotations.keySet(), "My\\Annotations\\PropertyOnly"); + assertDoesntContain(propertyAnnotations.keySet(), "My\\Annotations\\MethodAndAll"); + + Map methodAnnotations = AnnotationUtil.getAnnotationsOnTargetMap(getProject(), AnnotationTarget.METHOD); + assertContainsElements(methodAnnotations.keySet(), "My\\Annotations\\MethodAndAll"); + assertDoesntContain(methodAnnotations.keySet(), "My\\Annotations\\PropertyOnly"); + } + public void testThatImportForClassIsSuggestedForAliasImportClass() { myFixture.copyFileToProject("doctrine.php"); diff --git a/src/test/java/de/espend/idea/php/annotation/tests/util/fixtures/classes_targets.php b/src/test/java/de/espend/idea/php/annotation/tests/util/fixtures/classes_targets.php new file mode 100644 index 00000000..8cc8e6bc --- /dev/null +++ b/src/test/java/de/espend/idea/php/annotation/tests/util/fixtures/classes_targets.php @@ -0,0 +1,20 @@ +