[ui] IJPL-229294 Inspection for unresolved color names in .theme.json files

GitOrigin-RevId: f2b7cc3794843e8a95927d92bacc10780de6ce10
This commit is contained in:
Yuriy Artamonov
2026-02-09 11:16:55 +00:00
committed by intellij-monorepo-bot
parent 4564251081
commit ab3eea633b
9 changed files with 142 additions and 15 deletions
@@ -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": "<error descr="Cannot resolve symbol 'unregistered-color'">unregistered-color</error>",
"hoverBackground": "registered-color"
}
},
"ProjectGradients": {
"Group1": {
"DiagonalGradient": {
"Color1": "registered-color",
"Color2": "<error descr="Cannot resolve symbol 'unregistered-color'">unregistered-color</error>"
}
}
}
}
""".trimIndent())
myFixture.checkHighlighting()
}
}
@@ -0,0 +1,12 @@
<html>
<body>
Reports unresolved theme named colors in IntelliJ theme files (<code>*.theme.json</code>).
<p>
Named colors are defined in the <code>colors</code> section of the theme file and can be referenced
in the <code>ui</code> section or gradient definitions.
</p>
<p>
See <a href="https://plugins.jetbrains.com/docs/intellij/themes-metadata.html">Exposing Theme Metadata</a> for more information.
</p>
</body>
</html>
@@ -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"/>
<localInspection language="JSON" shortName="UnresolvedIntelliJThemeJsonNamedColor"
enabledByDefault="true" level="ERROR"
groupBundle="messages.DevKitBundle" groupPathKey="inspections.group.path" groupKey="inspections.group.code"
implementationClass="org.jetbrains.idea.devkit.themes.UnresolvedThemeJsonNamedColorInspection"
bundle="messages.DevKitThemesBundle" key="inspection.unresolved.theme.json.named.color.display.name"/>
<localInspection language="UAST" shortName="UnregisteredNamedColor"
projectType="INTELLIJ_PLUGIN"
groupBundle="messages.DevKitBundle" groupPathKey="inspections.group.path" groupKey="inspections.group.code"
@@ -19,6 +19,7 @@ theme.json.documentation.key.deprecated=Deprecated
theme.json.documentation.key.no.description=(no description)
theme.json.documentation.section.source.title=Source:
theme.json.documentation.section.since.title=Since:
inspection.unresolved.theme.json.named.color.display.name=Unresolved theme named color
inspection.unregistered.named.color.display.name=Unregistered named color
inspections.unregistered.named.color=<html>Named color key ''{0}'' is not registered in ''*.themeMetadata.json'' (<a href="https://plugins.jetbrains.com/docs/intellij/themes-metadata.html?from=DevkitUnregisteredNamedColorInspection">Documentation</a>)</html>
inspections.unregistered.named.color.fix.navigate.theme.metadata.file=Navigate to Theme metadata file
@@ -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;
@@ -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<JsonLiteral> {
private final String myName;
@@ -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();
}
}
@@ -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) {
@@ -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);
}
}
}
}
}
};
}
}