Skip to content

Commit 7408800

Browse files
authored
Merge pull request #317 from cedricziel/js-support
2 parents bfacb72 + ad3d3cb commit 7408800

24 files changed

+634
-5
lines changed

typo3-cms/src/main/java/com/cedricziel/idea/typo3/TYPO3CMSProjectComponent.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import com.intellij.openapi.extensions.PluginId;
1515
import com.intellij.openapi.project.Project;
1616
import com.intellij.openapi.vfs.VfsUtil;
17+
import com.intellij.psi.search.FilenameIndex;
18+
import com.intellij.psi.search.GlobalSearchScope;
1719
import com.intellij.util.indexing.FileBasedIndex;
1820
import org.jetbrains.annotations.NotNull;
1921
import org.jetbrains.annotations.Nullable;
@@ -89,7 +91,7 @@ public boolean isEnabled(@Nullable Project project) {
8991
}
9092

9193
private void checkProject() {
92-
if (!this.isEnabled(project) && !notificationIsDismissed() && containsTYPO3Libraries()) {
94+
if (!this.isEnabled(project) && !notificationIsDismissed() && containsPluginRelatedFiles()) {
9395
IdeHelper.notifyEnableMessage(project);
9496
}
9597
}
@@ -99,8 +101,8 @@ private boolean notificationIsDismissed() {
99101
return TYPO3CMSProjectSettings.getInstance(project).dismissEnableNotification;
100102
}
101103

102-
private boolean containsTYPO3Libraries() {
103-
104-
return VfsUtil.findRelativeFile(this.project.getBaseDir(), "vendor", "typo3") != null;
104+
private boolean containsPluginRelatedFiles() {
105+
return (VfsUtil.findRelativeFile(this.project.getBaseDir(), "vendor", "typo3") != null)
106+
|| FilenameIndex.getFilesByName(project, "ext_emconf.php", GlobalSearchScope.allScope(project)).length > 0;
105107
}
106108
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.cedricziel.idea.typo3.codeInspection;
2+
3+
import com.cedricziel.idea.typo3.TYPO3CMSProjectSettings;
4+
import com.intellij.codeInspection.LocalInspectionToolSession;
5+
import com.intellij.codeInspection.ProblemsHolder;
6+
import com.intellij.lang.javascript.inspections.JSInspection;
7+
import com.intellij.lang.javascript.psi.JSElementVisitor;
8+
import com.intellij.psi.PsiElementVisitor;
9+
import org.jetbrains.annotations.NotNull;
10+
11+
public abstract class PluginEnabledJsInspection extends JSInspection {
12+
@NotNull
13+
@Override
14+
protected PsiElementVisitor createVisitor(@NotNull ProblemsHolder problemsHolder, @NotNull LocalInspectionToolSession localInspectionToolSession) {
15+
if (!TYPO3CMSProjectSettings.getInstance(problemsHolder.getProject()).pluginEnabled) {
16+
return new JSElementVisitor() {
17+
};
18+
}
19+
20+
return buildRealVisitor(problemsHolder, localInspectionToolSession);
21+
}
22+
23+
@NotNull
24+
public abstract PsiElementVisitor buildRealVisitor(@NotNull ProblemsHolder problemsHolder, @NotNull LocalInspectionToolSession localInspectionToolSession);
25+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.cedricziel.idea.typo3.javaScript;
2+
3+
import com.cedricziel.idea.typo3.util.JavaScriptUtil;
4+
import com.intellij.codeInsight.lookup.LookupElement;
5+
import com.intellij.lang.javascript.completion.JSLookupPriority;
6+
import com.intellij.lang.javascript.completion.JSLookupUtilImpl;
7+
import com.intellij.lang.javascript.frameworks.modules.JSResolvableModuleReferenceContributor;
8+
import com.intellij.lang.javascript.psi.resolve.JSResolveResult;
9+
import com.intellij.openapi.project.DumbService;
10+
import com.intellij.psi.PsiElement;
11+
import com.intellij.psi.PsiFile;
12+
import com.intellij.psi.ResolveResult;
13+
import org.jetbrains.annotations.NotNull;
14+
15+
import java.util.HashSet;
16+
import java.util.List;
17+
import java.util.Set;
18+
19+
public class ModuleProvider extends JSResolvableModuleReferenceContributor {
20+
@NotNull
21+
@Override
22+
protected ResolveResult[] resolveElement(@NotNull PsiElement psiElement, @NotNull String s) {
23+
if (!s.startsWith(JavaScriptUtil.MODULE_PREFIX)) {
24+
return ResolveResult.EMPTY_ARRAY;
25+
}
26+
27+
List<PsiFile> moduleFilesFor = JavaScriptUtil.findModuleFilesFor(psiElement.getProject(), s);
28+
29+
if (moduleFilesFor.isEmpty()) {
30+
return ResolveResult.EMPTY_ARRAY;
31+
}
32+
33+
return JSResolveResult.toResolveResults(moduleFilesFor);
34+
}
35+
36+
@NotNull
37+
@Override
38+
protected Object[] getVariants(@NotNull PsiElement element) {
39+
Set<LookupElement> result = new HashSet<>();
40+
41+
JavaScriptUtil.getModuleMap(element.getProject()).forEach((name, file) -> {
42+
for (PsiFile psiFile : file) {
43+
LookupElement lookupItem = JSLookupUtilImpl.createPrioritizedLookupItem(psiFile, name, JSLookupPriority.MAX_PRIORITY);
44+
if (lookupItem != null) {
45+
result.add(lookupItem);
46+
}
47+
}
48+
});
49+
50+
return result.toArray();
51+
}
52+
53+
@Override
54+
public int getDefaultWeight() {
55+
return 25;
56+
}
57+
58+
public boolean isApplicable(@NotNull PsiElement host) {
59+
60+
return !DumbService.isDumb(host.getProject());
61+
}
62+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.cedricziel.idea.typo3.javaScript.codeInspection;
2+
3+
import com.cedricziel.idea.typo3.codeInspection.PluginEnabledJsInspection;
4+
import com.cedricziel.idea.typo3.util.JavaScriptUtil;
5+
import com.intellij.codeInspection.LocalInspectionToolSession;
6+
import com.intellij.codeInspection.ProblemsHolder;
7+
import com.intellij.lang.javascript.frameworks.modules.JSResolvableModuleReference;
8+
import com.intellij.lang.javascript.psi.JSElementVisitor;
9+
import com.intellij.lang.javascript.psi.JSLiteralExpression;
10+
import com.intellij.psi.PsiElementVisitor;
11+
import com.intellij.psi.PsiReference;
12+
import org.jetbrains.annotations.Nls;
13+
import org.jetbrains.annotations.NotNull;
14+
15+
public class MissingModuleJSInspection extends PluginEnabledJsInspection {
16+
@Nls(capitalization = Nls.Capitalization.Sentence)
17+
@NotNull
18+
@Override
19+
public String getDisplayName() {
20+
return "Missing JavaScript module";
21+
}
22+
23+
@NotNull
24+
@Override
25+
public PsiElementVisitor buildRealVisitor(@NotNull ProblemsHolder problemsHolder, @NotNull LocalInspectionToolSession localInspectionToolSession) {
26+
return new JSElementVisitor() {
27+
@Override
28+
public void visitJSLiteralExpression(JSLiteralExpression node) {
29+
PsiReference[] references = node.getReferences();
30+
for (PsiReference reference : references) {
31+
if (!(reference instanceof JSResolvableModuleReference)) {
32+
continue;
33+
}
34+
35+
JSResolvableModuleReference moduleReference = (JSResolvableModuleReference) reference;
36+
37+
String canonicalText = moduleReference.getCanonicalText();
38+
if (!canonicalText.startsWith(JavaScriptUtil.MODULE_PREFIX)) {
39+
super.visitJSLiteralExpression(node);
40+
41+
return;
42+
}
43+
44+
if (JavaScriptUtil.getModuleMap(node.getProject()).containsKey(canonicalText)) {
45+
super.visitJSLiteralExpression(node);
46+
47+
return;
48+
}
49+
50+
problemsHolder.registerProblem(node, String.format("Unknown JavaScript module \"%s\"", canonicalText));
51+
52+
return;
53+
}
54+
55+
super.visitJSLiteralExpression(node);
56+
}
57+
};
58+
}
59+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.cedricziel.idea.typo3.javaScript.codeInspection;
2+
3+
import com.cedricziel.idea.typo3.codeInspection.PluginEnabledPhpInspection;
4+
import com.cedricziel.idea.typo3.util.JavaScriptUtil;
5+
import com.intellij.codeInspection.ProblemsHolder;
6+
import com.intellij.patterns.PlatformPatterns;
7+
import com.intellij.patterns.PsiElementPattern;
8+
import com.intellij.psi.PsiElement;
9+
import com.intellij.psi.PsiElementVisitor;
10+
import com.intellij.psi.util.PsiTreeUtil;
11+
import com.jetbrains.php.lang.psi.elements.MethodReference;
12+
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
13+
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor;
14+
import org.jetbrains.annotations.Nls;
15+
import org.jetbrains.annotations.NotNull;
16+
17+
public class MissingModulePHPInspection extends PluginEnabledPhpInspection {
18+
@Nls(capitalization = Nls.Capitalization.Sentence)
19+
@NotNull
20+
@Override
21+
public String getDisplayName() {
22+
return "Missing JavaScript module";
23+
}
24+
25+
@NotNull
26+
@Override
27+
public String getShortName() {
28+
return "MissingModulePHP";
29+
}
30+
31+
@NotNull
32+
@Override
33+
public PsiElementVisitor buildRealVisitor(@NotNull ProblemsHolder problemsHolder, boolean b) {
34+
return new PhpElementVisitor() {
35+
@Override
36+
public void visitPhpStringLiteralExpression(StringLiteralExpression expression) {
37+
if (!getLoadRequireJsModulePattern().accepts(expression)) {
38+
super.visitPhpStringLiteralExpression(expression);
39+
return;
40+
}
41+
42+
PsiElement firstParent = PsiTreeUtil.findFirstParent(expression, e -> e instanceof MethodReference);
43+
if (!(firstParent instanceof MethodReference)) {
44+
super.visitPhpStringLiteralExpression(expression);
45+
return;
46+
}
47+
48+
MethodReference methodReference = (MethodReference) firstParent;
49+
if (methodReference.getName() == null || !methodReference.getName().equals("loadRequireJsModule")) {
50+
super.visitPhpStringLiteralExpression(expression);
51+
return;
52+
}
53+
54+
if (expression.getPrevPsiSibling() instanceof StringLiteralExpression) {
55+
super.visitPhpStringLiteralExpression(expression);
56+
return;
57+
}
58+
59+
if (JavaScriptUtil.getModuleMap(expression.getProject()).containsKey(expression.getContents())) {
60+
return;
61+
}
62+
63+
problemsHolder.registerProblem(expression, String.format("Unknown JavaScript module \"%s\"", expression.getContents()));
64+
}
65+
};
66+
}
67+
68+
@NotNull
69+
private PsiElementPattern.Capture<PsiElement> getLoadRequireJsModulePattern() {
70+
return PlatformPatterns.psiElement().withSuperParent(
71+
2,
72+
PlatformPatterns.psiElement(MethodReference.class)
73+
);
74+
}
75+
}

typo3-cms/src/main/java/com/cedricziel/idea/typo3/util/ExtensionUtility.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ private static class ExtEmconfNamespacesVisitor extends PsiRecursiveElementVisit
114114
private List<String> ns;
115115

116116
@Override
117-
public void visitElement(PsiElement element) {
117+
public void visitElement(@NotNull PsiElement element) {
118118

119119
if (PlatformPatterns.psiElement(StringLiteralExpression.class).withParent(
120120
PlatformPatterns.psiElement(PhpElementTypes.ARRAY_KEY).withParent(
@@ -160,6 +160,11 @@ public String[] getNamespaces() {
160160
}
161161
}
162162

163+
@Nullable
164+
public static String findExtensionKeyFromFile(@NotNull PsiFile file) {
165+
return findExtensionKeyFromFile(file.getVirtualFile());
166+
}
167+
163168
@Nullable
164169
public static String findExtensionKeyFromFile(@NotNull VirtualFile file) {
165170
VirtualFile extensionRootFolder = FilesystemUtil.findExtensionRootFolder(file);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.cedricziel.idea.typo3.util;
2+
3+
import com.intellij.openapi.project.Project;
4+
import com.intellij.openapi.util.Key;
5+
import com.intellij.openapi.vfs.VirtualFile;
6+
import com.intellij.psi.PsiFile;
7+
import com.intellij.psi.PsiManager;
8+
import com.intellij.psi.search.FilenameIndex;
9+
import com.intellij.psi.search.GlobalSearchScope;
10+
import com.intellij.psi.util.CachedValue;
11+
import com.intellij.psi.util.CachedValueProvider;
12+
import com.intellij.psi.util.CachedValuesManager;
13+
import com.intellij.psi.util.PsiModificationTracker;
14+
import org.apache.commons.lang.WordUtils;
15+
import org.jetbrains.annotations.NotNull;
16+
import org.jetbrains.annotations.Nullable;
17+
18+
import java.util.*;
19+
import java.util.stream.Collectors;
20+
21+
public class JavaScriptUtil {
22+
23+
public static final String MODULE_PREFIX = "TYPO3/CMS/";
24+
public static final String SIGNIFICANT_PATH = "Resources/Public/JavaScript/";
25+
public static final Key<String> MODULE_NAME_DATA_KEY = Key.create("t3module");
26+
public static final Key<CachedValue<Map<String, List<PsiFile>>>> MODULE_MAP = Key.create("t3modulemap");
27+
28+
@NotNull
29+
public static Map<String, List<PsiFile>> getModuleMap(@NotNull Project project) {
30+
return CachedValuesManager.getManager(project).getCachedValue(
31+
project,
32+
MODULE_MAP,
33+
() -> CachedValueProvider.Result.create(doGetModuleMap(project), PsiModificationTracker.MODIFICATION_COUNT),
34+
false
35+
);
36+
}
37+
38+
@NotNull
39+
private static Map<String, List<PsiFile>> doGetModuleMap(@NotNull Project project) {
40+
Map<String, List<PsiFile>> map = new HashMap<>();
41+
for (PsiFile psiFile : findModuleFiles(project)) {
42+
if (psiFile.getUserData(MODULE_NAME_DATA_KEY) != null) {
43+
map.put(psiFile.getUserData(MODULE_NAME_DATA_KEY), Collections.singletonList(psiFile));
44+
45+
continue;
46+
}
47+
48+
String name = calculateModuleName(psiFile);
49+
if (name != null) {
50+
psiFile.putUserData(MODULE_NAME_DATA_KEY, name);
51+
map.put(name, Collections.singletonList(psiFile));
52+
}
53+
}
54+
55+
return map;
56+
}
57+
58+
@NotNull
59+
public static List<PsiFile> findModuleFilesFor(@NotNull Project project, @NotNull String identifier) {
60+
61+
return getModuleMap(project).getOrDefault(identifier, Collections.emptyList());
62+
}
63+
64+
@NotNull
65+
public static List<PsiFile> findModuleFiles(@NotNull Project project) {
66+
67+
return FilenameIndex
68+
.getAllFilesByExt(project, "js", GlobalSearchScope.projectScope(project))
69+
.stream()
70+
.filter(f -> f.getPath().contains(SIGNIFICANT_PATH))
71+
.map(f -> PsiManager.getInstance(project).findFile(f))
72+
.filter(Objects::nonNull)
73+
.collect(Collectors.toList());
74+
}
75+
76+
@Nullable
77+
public static String calculateModuleName(@NotNull PsiFile file) {
78+
String extensionKey = ExtensionUtility.findExtensionKeyFromFile(file);
79+
if (extensionKey == null) {
80+
return null;
81+
}
82+
83+
VirtualFile virtualFile = file.getVirtualFile();
84+
String modLocalName = virtualFile.getPath();
85+
String nameWithoutExtension = virtualFile.getNameWithoutExtension();
86+
87+
String[] pathSplit = modLocalName.split(SIGNIFICANT_PATH);
88+
if (pathSplit.length <= 1) {
89+
return null;
90+
}
91+
92+
String[] nameSplit = pathSplit[1].split(nameWithoutExtension);
93+
if (nameSplit.length <= 1) {
94+
return null;
95+
}
96+
97+
return MODULE_PREFIX + normalizeExtensionKeyForJs(extensionKey) + "/" + nameSplit[0] + nameWithoutExtension;
98+
}
99+
100+
@NotNull
101+
public static String normalizeExtensionKeyForJs(@NotNull String extensionKey) {
102+
return WordUtils.capitalizeFully(extensionKey, new char[]{'_'}).replaceAll("_", "");
103+
}
104+
}

0 commit comments

Comments
 (0)