Skip to content

feat: similified no-sldshook-fallback-for-lwctoken Eslint v9 #246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/eslint-plugin-slds/src/config/rule-messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ lwc-token-to-slds-hook:
errorWithStyleHooks: "The '{{oldValue}}' design token is deprecated. Replace it with the SLDS 2 '{{newValue}}' styling hook and set the fallback to '{{oldValue}}'. For more info, see Global Styling Hooks on lightningdesignsystem.com."
errorWithNoRecommendation: "The '{{oldValue}}' design token is deprecated. For more info, see the New Global Styling Hook Guidance on lightningdesignsystem.com."

no-sldshook-fallback-for-lwctoken:
description: "Avoid using --slds styling hooks as fallback values for --lwc tokens."
url: "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-sldshook-fallback-for-lwctoken"
type: "problem"
messages:
unsupportedFallback: "Remove the {{sldsToken}} styling hook that is used as a fallback value for {{lwcToken}}."

no-unsupported-hooks-slds2:
description: "Identifies styling hooks that aren't present in SLDS 2. They must be replaced with styling hooks that have a similar effect, or they must be removed."
url: "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-unsupported-hooks-slds2"
Expand Down
3 changes: 3 additions & 0 deletions packages/eslint-plugin-slds/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import noDeprecatedSldsClasses from './rules/v9/no-deprecated-slds-classes';
import noDeprecatedTokensSlds1 from './rules/v9/no-deprecated-tokens-slds1';
import lwcTokenToSldsHook from './rules/v9/lwc-token-to-slds-hook';
import enforceSdsToSldsHooks from './rules/v9/enforce-sds-to-slds-hooks';
import noSldshookFallbackForLwctoken from './rules/v9/no-sldshook-fallback-for-lwctoken';
import noUnsupportedHooksSlds2 from './rules/v9/no-unsupported-hooks-slds2';
import noSldsVarWithoutFallback from './rules/v9/no-slds-var-without-fallback';
import noSldsNamespaceForCustomHooks from './rules/v9/no-slds-namespace-for-custom-hooks';
Expand All @@ -25,6 +26,7 @@ const rules = {
"no-deprecated-tokens-slds1": noDeprecatedTokensSlds1,
"lwc-token-to-slds-hook": lwcTokenToSldsHook,
"enforce-sds-to-slds-hooks": enforceSdsToSldsHooks,
"no-sldshook-fallback-for-lwctoken": noSldshookFallbackForLwctoken,
"no-unsupported-hooks-slds2": noUnsupportedHooksSlds2,
"no-slds-var-without-fallback": noSldsVarWithoutFallback,
"no-slds-namespace-for-custom-hooks": noSldsNamespaceForCustomHooks
Expand Down Expand Up @@ -78,6 +80,7 @@ Object.assign(plugin.configs, {
"@salesforce-ux/slds/lwc-token-to-slds-hook": "error",
"@salesforce-ux/slds/enforce-bem-usage": "warn",
"@salesforce-ux/slds/enforce-sds-to-slds-hooks": "warn",
"@salesforce-ux/slds/no-sldshook-fallback-for-lwctoken": "warn",
"@salesforce-ux/slds/no-unsupported-hooks-slds2": "warn",
"@salesforce-ux/slds/no-slds-var-without-fallback": "warn",
"@salesforce-ux/slds/no-slds-namespace-for-custom-hooks": "warn"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Rule } from 'eslint';
import metadata from '@salesforce-ux/sds-metadata';
import ruleMessages from '../../config/rule-messages.yml';

const ruleConfig = ruleMessages['no-sldshook-fallback-for-lwctoken'];
const { type, description, url, messages } = ruleConfig;

const sldsPlusStylingHooks = metadata.sldsPlusStylingHooks;

// Generate values to hooks mapping using only global hooks
// shared hooks are private/ undocumented APIs, so they should not be recommended to customers
// Ref this thread: https://salesforce-internal.slack.com/archives/C071J0Q3FNV/p1743010620921339?thread_ts=1743009353.385429&cid=C071J0Q3FNV
const allSldsHooks = [...sldsPlusStylingHooks.global, ...sldsPlusStylingHooks.component];
const allSldsHooksSet = new Set(allSldsHooks);

/**
* Check if using an SLDS hook as fallback for LWC token is unsupported
*/
function hasUnsupportedFallback(lwcToken: string, sldsToken: string): boolean {
// Convert --sds- to --slds- if needed
const normalizedSldsToken = sldsToken.replace('--sds-', '--slds-');

return lwcToken.startsWith('--lwc-')
&& normalizedSldsToken.startsWith('--slds-')
&& allSldsHooksSet.has(normalizedSldsToken);
}

export default {
meta: {
type,
docs: {
description,
recommended: true,
url,
},
messages,
},

create(context) {
return {
// Handle LWC tokens inside var() functions: var(--lwc-*, ...)
"Function[name='var'] Identifier[name=/^--lwc-/]"(node) {
const lwcToken = node.name;

// Get the var() function node that contains this identifier
const varFunctionNode = context.sourceCode.getAncestors(node).at(-1);
if (!varFunctionNode) return;

//access children to find fallback
const varFunctionChildren = (varFunctionNode as any).children;
if (!varFunctionChildren) return;

// Find comma operator and the Raw node after it
let foundComma = false;
let fallbackRawNode = null;

for (const child of varFunctionChildren) {
if (child.type === 'Operator' && child.value === ',') {
foundComma = true;
continue;
}
if (foundComma && child.type === 'Raw') {
fallbackRawNode = child;
break;
}
}

if (!fallbackRawNode) return;

// Extract SLDS token from the Raw node value
const fallbackValue = fallbackRawNode.value.trim();
const varMatch = fallbackValue.match(/var\(([^,)]+)/);
if (!varMatch) return;

const sldsToken = varMatch[1];

if (hasUnsupportedFallback(lwcToken, sldsToken)) {
context.report({
node,
messageId: 'unsupportedFallback',
data: { lwcToken, sldsToken }
});
}
}
};
},
} as Rule.RuleModule;
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
const rule = require('../../src/rules/v9/no-sldshook-fallback-for-lwctoken').default;
const { RuleTester } = require('eslint');

let cssPlugin;
try {
cssPlugin = require('@eslint/css').default || require('@eslint/css');
} catch (e) {
cssPlugin = require('@eslint/css');
}

const ruleTester = new RuleTester({
plugins: {
css: cssPlugin,
},
language: 'css/css',
});

ruleTester.run('no-sldshook-fallback-for-lwctoken', rule, {
valid: [
// Non-LWC tokens should be ignored
{
code: `.example { color: var(--myapp-color-background-1, var(--slds-g-color-border-1)); }`,
filename: 'test.css',
},
// LWC tokens with non-SLDS fallbacks should be valid
{
code: `.example { color: var(--lwc-color-background-1, var(--myapp-color-border-1)); }`,
filename: 'test.css',
},
// SLDS tokens without LWC prefix should be valid
{
code: `.example { color: var(--slds-g-color-border-1); }`,
filename: 'test.css',
},
// LWC tokens without fallback should be valid
{
code: `.example { color: var(--lwc-color-background-1); }`,
filename: 'test.css',
},
// LWC tokens with static value fallbacks should be valid
{
code: `.example { color: var(--lwc-color-background-1, #333); }`,
filename: 'test.css',
},
// Non-var functions should be ignored
{
code: `.example { color: red; background: #fff; }`,
filename: 'test.css',
},
// LWC tokens with fallback to tokens not in SLDS metadata should be valid
{
code: `.example { color: var(--lwc-color-background-1, var(--slds-unknown-token)); }`,
filename: 'test.css',
}
],

invalid: [
// LWC token with SLDS hook fallback - basic case
{
code: `.example { color: var(--lwc-color-background-1, var(--slds-g-color-border-1)); }`,
filename: 'test.css',
errors: [{
messageId: 'unsupportedFallback',
type: 'Identifier',
data: {
lwcToken: '--lwc-color-background-1',
sldsToken: '--slds-g-color-border-1'
}
}]
},

// Complex selector with LWC token using SLDS fallback
{
code: `.container .example:hover { background: var(--lwc-color-background-1, var(--slds-g-color-border-1)); }`,
filename: 'test.css',
errors: [{
messageId: 'unsupportedFallback',
type: 'Identifier',
data: {
lwcToken: '--lwc-color-background-1',
sldsToken: '--slds-g-color-border-1'
}
}]
},

// Multiple LWC tokens with SLDS fallbacks in same declaration
{
code: `.example {
color: var(--lwc-color-background-1, var(--slds-g-color-border-1));
background: var(--lwc-color-background-2, var(--slds-g-color-border-2));
}`,
filename: 'test.css',
errors: [
{
messageId: 'unsupportedFallback',
type: 'Identifier',
data: {
lwcToken: '--lwc-color-background-1',
sldsToken: '--slds-g-color-border-1'
}
},
{
messageId: 'unsupportedFallback',
type: 'Identifier',
data: {
lwcToken: '--lwc-color-background-2',
sldsToken: '--slds-g-color-border-2'
}
}
]
},

// Nested var() functions - deeper nesting (matching stylelint test case)
{
code: `.example { color: var(--lwc-color-background-1, var(--slds-g-color-border-1, var(--lwc-color-background-2))); }`,
filename: 'test.css',
errors: [{
messageId: 'unsupportedFallback',
type: 'Identifier',
data: {
lwcToken: '--lwc-color-background-1',
sldsToken: '--slds-g-color-border-1'
}
}]
},

// Mixed valid and invalid - some with SLDS fallbacks, some without
{
code: `.example {
color: var(--custom-color);
background: var(--lwc-color-background-1, var(--slds-g-color-border-1));
border: var(--lwc-color-background-2, #static-value);
}`,
filename: 'test.css',
errors: [{
messageId: 'unsupportedFallback',
type: 'Identifier',
data: {
lwcToken: '--lwc-color-background-1',
sldsToken: '--slds-g-color-border-1'
}
}]
},

// SDS token converted to SLDS - testing toSldsToken function
{
code: `.example { color: var(--lwc-color-background-1, var(--sds-g-color-border-1)); }`,
filename: 'test.css',
errors: [{
messageId: 'unsupportedFallback',
type: 'Identifier',
data: {
lwcToken: '--lwc-color-background-1',
sldsToken: '--sds-g-color-border-1'
}
}]
},

// LWC token in calc() function
{
code: `.example { width: calc(100% - var(--lwc-spacing-4, var(--slds-g-spacing-4))); }`,
filename: 'test.css',
errors: [{
messageId: 'unsupportedFallback',
type: 'Identifier',
data: {
lwcToken: '--lwc-spacing-4',
sldsToken: '--slds-g-spacing-4'
}
}]
}
]
});
6 changes: 0 additions & 6 deletions packages/stylelint-plugin-slds/stylelint.rules.internal.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@
"severity": "warning"
}
],
"slds/no-sldshook-fallback-for-lwctoken": [
true,
{
"severity": "warning"
}
],
"slds/reduce-annotations": [
true,
{
Expand Down
6 changes: 0 additions & 6 deletions packages/stylelint-plugin-slds/stylelint.rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@
"severity": "warning"
}
],
"slds/no-sldshook-fallback-for-lwctoken": [
true,
{
"severity": "warning"
}
],
"slds/reduce-annotations": [
true,
{
Expand Down