diff --git a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/UnresolvedThemeKeyInspectionTest.kt b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/JsonThemeInspectionTest.kt similarity index 64% rename from plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/UnresolvedThemeKeyInspectionTest.kt rename to plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/JsonThemeInspectionTest.kt index 4e51a5c7cc6b..b29ec578d087 100644 --- a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/UnresolvedThemeKeyInspectionTest.kt +++ b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/JsonThemeInspectionTest.kt @@ -2,13 +2,17 @@ package org.jetbrains.idea.devkit.inspections import com.intellij.testFramework.fixtures.BasePlatformTestCase +import org.jetbrains.idea.devkit.themes.UnresolvedThemeJsonNamedColorInspection import org.jetbrains.idea.devkit.themes.UnresolvedIntelliJThemeKeyInspection -class UnresolvedThemeKeyInspectionTest : BasePlatformTestCase() { +class JsonThemeInspectionTest : BasePlatformTestCase() { override fun setUp() { super.setUp() - myFixture.enableInspections(UnresolvedIntelliJThemeKeyInspection::class.java) + myFixture.enableInspections( + UnresolvedIntelliJThemeKeyInspection::class.java, + UnresolvedThemeJsonNamedColorInspection::class.java + ) } fun testRegularKeys() { @@ -69,4 +73,32 @@ class UnresolvedThemeKeyInspectionTest : BasePlatformTestCase() { """.trimIndent()) myFixture.checkHighlighting() } + + fun testColorNamesInThemeFiles() { + myFixture.configureByText("test.theme.json", """ + { + "name": "Test Theme", + "colors": { + "registered-color": "#000000" + }, + "ui": { + "ActionButton.focusedBorderColor": "registered-color", + "ActionButton.separatorColor": "#ffffff", + "ActionButton": { + "pressedBackground": "unregistered-color", + "hoverBackground": "registered-color" + } + }, + "ProjectGradients": { + "Group1": { + "DiagonalGradient": { + "Color1": "registered-color", + "Color2": "unregistered-color" + } + } + } + } + """.trimIndent()) + myFixture.checkHighlighting() + } } \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit.themes/resources/inspectionDescriptions/UnresolvedIntelliJThemeJsonNamedColor.html b/plugins/devkit/intellij.devkit.themes/resources/inspectionDescriptions/UnresolvedIntelliJThemeJsonNamedColor.html new file mode 100644 index 000000000000..fa672bd7a64e --- /dev/null +++ b/plugins/devkit/intellij.devkit.themes/resources/inspectionDescriptions/UnresolvedIntelliJThemeJsonNamedColor.html @@ -0,0 +1,12 @@ + + +Reports unresolved theme named colors in IntelliJ theme files (*.theme.json). +

+ Named colors are defined in the colors section of the theme file and can be referenced + in the ui section or gradient definitions. +

+

+ See Exposing Theme Metadata for more information. +

+ + \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit.themes/resources/intellij.devkit.themes.xml b/plugins/devkit/intellij.devkit.themes/resources/intellij.devkit.themes.xml index dd54cee75b5d..b74309a7946f 100644 --- a/plugins/devkit/intellij.devkit.themes/resources/intellij.devkit.themes.xml +++ b/plugins/devkit/intellij.devkit.themes/resources/intellij.devkit.themes.xml @@ -20,6 +20,11 @@ groupBundle="messages.DevKitBundle" groupPathKey="inspections.group.path" groupKey="inspections.group.code" implementationClass="org.jetbrains.idea.devkit.themes.UnresolvedIntelliJThemeKeyInspection" bundle="messages.DevKitThemesBundle" key="inspection.unresolved.theme.key.display.name"/> + Named color key ''{0}'' is not registered in ''*.themeMetadata.json'' (Documentation) inspections.unregistered.named.color.fix.navigate.theme.metadata.file=Navigate to Theme metadata file diff --git a/plugins/devkit/intellij.devkit.themes/src/ThemeColorAnnotator.java b/plugins/devkit/intellij.devkit.themes/src/ThemeColorAnnotator.java index dac7d2e3f539..e005a34a97b1 100644 --- a/plugins/devkit/intellij.devkit.themes/src/ThemeColorAnnotator.java +++ b/plugins/devkit/intellij.devkit.themes/src/ThemeColorAnnotator.java @@ -52,7 +52,7 @@ final class ThemeColorAnnotator implements Annotator, DumbAware { JsonStringLiteral literal = (JsonStringLiteral)element; holder.newSilentAnnotation(HighlightSeverity.INFORMATION) - .gutterIconRenderer(new MyRenderer(literal.getValue(), literal)).create(); + .gutterIconRenderer(new MyRenderer(literal.getValue(), literal)).create(); } private static boolean isColorLineMarkerProviderEnabled() { @@ -72,18 +72,17 @@ final class ThemeColorAnnotator implements Annotator, DumbAware { return isColorCode(text) || isNamedColor(text); } - private static boolean isNamedColor(String text) { + static boolean isNamedColor(String text) { return StringUtil.isLatinAlphanumeric(text); } - private static boolean isColorCode(@Nullable String text) { + static boolean isColorCode(@Nullable String text) { if (!StringUtil.startsWithChar(text, '#')) return false; //noinspection ConstantConditions - StringUtil#startsWithChar checks for null if (text.length() != HEX_COLOR_LENGTH_RGB && text.length() != HEX_COLOR_LENGTH_RGBA) return false; return COLOR_HEX_PATTERN_RGB.matcher(text).matches() || COLOR_HEX_PATTERN_RGBA.matcher(text).matches(); } - private static final class MyRenderer extends GutterIconRenderer implements DumbAware { private static final int ICON_SIZE = 12; diff --git a/plugins/devkit/intellij.devkit.themes/src/ThemeJsonNamedColorPsiReference.java b/plugins/devkit/intellij.devkit.themes/src/ThemeJsonNamedColorPsiReference.java index f82f4295b4da..07253a08928c 100644 --- a/plugins/devkit/intellij.devkit.themes/src/ThemeJsonNamedColorPsiReference.java +++ b/plugins/devkit/intellij.devkit.themes/src/ThemeJsonNamedColorPsiReference.java @@ -14,7 +14,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; -import java.util.regex.Pattern; final class ThemeJsonNamedColorPsiReference extends JsonSchemaBaseReference { private final String myName; diff --git a/plugins/devkit/intellij.devkit.themes/src/ThemeJsonNamedColorPsiReferenceProvider.java b/plugins/devkit/intellij.devkit.themes/src/ThemeJsonNamedColorPsiReferenceProvider.java index 0a770dd05080..d7d9f632f7f1 100644 --- a/plugins/devkit/intellij.devkit.themes/src/ThemeJsonNamedColorPsiReferenceProvider.java +++ b/plugins/devkit/intellij.devkit.themes/src/ThemeJsonNamedColorPsiReferenceProvider.java @@ -3,25 +3,60 @@ package org.jetbrains.idea.devkit.themes; import com.intellij.json.psi.JsonLiteral; import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonStringLiteral; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceProvider; import com.intellij.util.ProcessingContext; import org.jetbrains.annotations.NotNull; -/** - * @author Konstantin Bulenkov - */ -class ThemeJsonNamedColorPsiReferenceProvider extends PsiReferenceProvider { +import java.util.regex.Pattern; + +final class ThemeJsonNamedColorPsiReferenceProvider extends PsiReferenceProvider { + private static final Pattern COLOR_N_PATTERN = Pattern.compile("Color\\d+"); + @Override public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { if (!(element instanceof JsonLiteral literal)) return PsiReference.EMPTY_ARRAY; + if (element instanceof JsonStringLiteral + && ThemeColorAnnotator.isColorCode(((JsonStringLiteral)element).getValue())) { + return PsiReference.EMPTY_ARRAY; + } + PsiElement parent = literal.getParent(); - if (!(parent instanceof JsonProperty property)) return PsiReference.EMPTY_ARRAY; + if (parent instanceof JsonProperty property) { + String name = property.getName(); + if (property.getValue() == literal) { // inside value of property + if (isKeyInteresting(name)) { + return new PsiReference[]{new ThemeJsonNamedColorPsiReference(literal)}; + } - if (property.getValue() != literal) return PsiReference.EMPTY_ARRAY; + PsiElement grandParent = property.getParent(); + if (grandParent != null) { + PsiElement greatGrandParent = grandParent.getParent(); + if (greatGrandParent instanceof JsonProperty parentProperty) { + String parentName = parentProperty.getName(); + if (COLOR_N_PATTERN.matcher(parentName).matches() || isKeyInteresting(parentName)) { + return new PsiReference[]{new ThemeJsonNamedColorPsiReference(literal)}; + } + } + } + } + } - return new PsiReference[]{new ThemeJsonNamedColorPsiReference(literal)}; + return PsiReference.EMPTY_ARRAY; + } + + private static boolean isKeyInteresting(String name) { + return name.endsWith("Foreground") + || name.endsWith("Background") + || name.endsWith("Color") + || name.endsWith(".foreground") + || name.endsWith(".background") + || name.endsWith("color") + || "foreground".equals(name) + || "background".equals(name) + || COLOR_N_PATTERN.matcher(name).matches(); } } diff --git a/plugins/devkit/intellij.devkit.themes/src/ThemeJsonPsiReferenceContributor.java b/plugins/devkit/intellij.devkit.themes/src/ThemeJsonPsiReferenceContributor.java index 472e7aebeec7..380500aeb3da 100644 --- a/plugins/devkit/intellij.devkit.themes/src/ThemeJsonPsiReferenceContributor.java +++ b/plugins/devkit/intellij.devkit.themes/src/ThemeJsonPsiReferenceContributor.java @@ -10,11 +10,13 @@ import com.intellij.psi.PsiReferenceRegistrar; import com.intellij.util.ProcessingContext; import org.jetbrains.annotations.NotNull; +import static com.intellij.patterns.PlatformPatterns.psiElement; + final class ThemeJsonPsiReferenceContributor extends PsiReferenceContributor { @Override public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { registrar.registerReferenceProvider( - PlatformPatterns.psiElement(JsonStringLiteral.class) + psiElement(JsonStringLiteral.class) .inVirtualFile(PlatformPatterns.virtualFile().with(new PatternCondition<>("theme.json") { @Override public boolean accepts(@NotNull VirtualFile file, ProcessingContext context) { diff --git a/plugins/devkit/intellij.devkit.themes/src/UnresolvedThemeJsonNamedColorInspection.java b/plugins/devkit/intellij.devkit.themes/src/UnresolvedThemeJsonNamedColorInspection.java new file mode 100644 index 000000000000..bf059fe1791b --- /dev/null +++ b/plugins/devkit/intellij.devkit.themes/src/UnresolvedThemeJsonNamedColorInspection.java @@ -0,0 +1,42 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.idea.devkit.themes; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.json.psi.JsonElementVisitor; +import com.intellij.json.psi.JsonLiteral; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiReference; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class UnresolvedThemeJsonNamedColorInspection extends LocalInspectionTool { + @Override + public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + if (!ThemeJsonUtil.isThemeFilename(holder.getFile().getName())) return PsiElementVisitor.EMPTY_VISITOR; + + return new JsonElementVisitor() { + @Override + public void visitLiteral(@NotNull JsonLiteral literal) { + if (literal.getTextLength() < 2) return; + for (PsiReference reference : literal.getReferences()) { + if (reference instanceof ThemeJsonNamedColorPsiReference) { + if (reference.resolve() == null) { + TextRange range = reference.getRangeInElement(); + if (range.isEmpty() && range.getStartOffset() == 1 && "\"\"".equals(literal.getText())) { + String message = ProblemsHolder.unresolvedReferenceMessage(reference); + holder.registerProblem(literal, message, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL, TextRange.create(0, 2)); + } + else { + holder.registerProblem(reference); + } + } + } + } + } + }; + } +}