Skip to content

Commit 6e054c4

Browse files
authored
build: fix theming api lint rule (#22011)
Reworks the `theme-mixin-api` lint rule to account for the Sass module changes.
1 parent 72d7c68 commit 6e054c4

File tree

2 files changed

+46
-23
lines changed

2 files changed

+46
-23
lines changed

src/material/expansion/_expansion-theme.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191

9292
@mixin theme($theme-or-color-config) {
9393
$theme: theming.private-legacy-get-theme($theme-or-color-config);
94-
@include theming.private-check-duplicate-theme-styles($theme, 'mat-expansion-panel') {
94+
@include theming.private-check-duplicate-theme-styles($theme, 'mat-expansion') {
9595
$color: theming.get-color-config($theme);
9696
$density: theming.get-density-config($theme);
9797
$typography: theming.get-typography-config($theme);

tools/stylelint/theme-mixin-api.ts

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {createPlugin, Plugin, utils} from 'stylelint';
2+
import {basename} from 'path';
23
import {
34
AtRule,
45
atRule,
@@ -13,8 +14,7 @@ import {
1314
const ruleName = 'material/theme-mixin-api';
1415

1516
/** Regular expression that matches all theme mixins. */
16-
const themeMixinRegex =
17-
/^(?:(mat-.+)-(density)|(mat-.+)-(density|color|typography|theme))\((.*)\)$/;
17+
const themeMixinRegex = /^(density|color|typography|theme)\((.*)\)$/;
1818

1919
/**
2020
* Stylelint plugin which ensures that theme mixins have a consistent API. Besides
@@ -31,13 +31,14 @@ const themeMixinRegex =
3131
*/
3232
const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) => {
3333
return (root: Root, result: Result) => {
34-
if (!isEnabled) {
34+
const componentName = getComponentNameFromPath(root.source!.input.file!);
35+
36+
if (!componentName || !isEnabled) {
3537
return;
3638
}
3739

3840
root.walkAtRules('mixin', node => {
39-
if (node.params.startsWith('_') || node.params.startsWith('mat-private-') ||
40-
node.params.startsWith('mat-mdc-private-')) {
41+
if (node.params.startsWith('_') || node.params.startsWith('private-')) {
4142
// This is a private mixins that isn't intended to be consumed outside of our own code.
4243
return;
4344
}
@@ -47,24 +48,22 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
4748
return;
4849
}
4950

50-
// Name of the component with prefix. e.g. `mat-mdc-button` or `mat-slide-toggle`.
51-
const componentName = matches[1] || matches[3];
5251
// Type of the theme mixin. e.g. `density`, `color`, `theme`.
53-
const type = matches[2] || matches[4];
52+
const type = matches[1];
5453
// Naively assumes that mixin arguments can be easily retrieved by splitting based on
5554
// a comma. This is not always correct because Sass maps can be constructed in parameters.
5655
// These would contain commas that throw of the argument retrieval. It's acceptable that
5756
// this rule will fail in such edge-cases. There is no AST for `postcss.AtRule` params.
58-
const args = matches[5].split(',');
57+
const args = matches[2].split(',').map(arg => arg.trim());
5958

6059
if (type === 'theme') {
61-
validateThemeMixin(node, componentName, args);
60+
validateThemeMixin(node, args);
6261
} else {
6362
validateIndividualSystemMixins(node, type, args);
6463
}
6564
});
6665

67-
function validateThemeMixin(node: AtRule, componentName: string, args: string[]) {
66+
function validateThemeMixin(node: AtRule, args: string[]) {
6867
if (args.length !== 1) {
6968
reportError(node, 'Expected theme mixin to only declare a single argument.');
7069
} else if (args[0] !== '$theme-or-color-config') {
@@ -76,9 +75,9 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
7675
}
7776

7877
const themePropName = `$theme`;
79-
const legacyColorExtractExpr = `mat-private-legacy-get-theme($theme-or-color-config)`;
78+
const legacyColorExtractExpr = `theming.private-legacy-get-theme($theme-or-color-config)`;
8079
const duplicateStylesCheckExpr =
81-
`mat-private-check-duplicate-theme-styles(${themePropName}, '${componentName}')`;
80+
`theming.private-check-duplicate-theme-styles(${themePropName}, '${componentName}')`;
8281

8382
let legacyConfigDecl: Declaration|null = null;
8483
let duplicateStylesCheck: AtRule|null = null;
@@ -155,14 +154,13 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
155154
}
156155

157156
const expectedProperty = type === 'density' ? '$density-scale' : '$config';
158-
const expectedValues = [`mat-get-${type}-config($config-or-theme)`];
159-
if (type === 'typography') {
160-
expectedValues.shift();
161-
expectedValues.unshift(
162-
'mat-private-typography-to-2014-config(mat-get-typography-config($config-or-theme))',
163-
'mat-get-typography-config($config-or-theme)',
164-
);
165-
}
157+
const expectedValues = type === 'typography' ?
158+
[
159+
'typography.private-typography-to-2014-config(' +
160+
'theming.get-typography-config($config-or-theme))',
161+
'theming.get-typography-config($config-or-theme)'
162+
] :
163+
[`theming.get-${type}-config($config-or-theme)`];
166164
let configExtractionNode: Declaration|null = null;
167165
let nonCommentNodeCount = 0;
168166

@@ -172,7 +170,8 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
172170
nonCommentNodeCount++;
173171
}
174172

175-
if (currentNode.type === 'decl' && expectedValues.includes(currentNode.value)) {
173+
if (currentNode.type === 'decl' &&
174+
expectedValues.includes(stripNewlinesAndIndentation(currentNode.value))) {
176175
configExtractionNode = currentNode;
177176
break;
178177
}
@@ -204,6 +203,30 @@ const plugin = (isEnabled: boolean, _options: never, context: {fix: boolean}) =>
204203
};
205204
};
206205

206+
/** Figures out the name of the component from a file path. */
207+
function getComponentNameFromPath(filePath: string): string|null {
208+
const match = basename(filePath).match(/_?(.*)-theme\.scss$/);
209+
210+
if (!match) {
211+
return null;
212+
}
213+
214+
let prefix = '';
215+
216+
if (filePath.includes('material-experimental')) {
217+
prefix = 'mat-mdc-';
218+
} else if (filePath.includes('material')) {
219+
prefix = 'mat-';
220+
}
221+
222+
return prefix + match[1];
223+
}
224+
225+
/** Strips newlines from a string and any whitespace immediately after it. */
226+
function stripNewlinesAndIndentation(value: string): string {
227+
return value.replace(/(\r|\n)\s+/g, '');
228+
}
229+
207230
// Note: We need to cast the value explicitly to `Plugin` because the stylelint types
208231
// do not type the context parameter. https://stylelint.io/developer-guide/rules#add-autofix
209232
module.exports = createPlugin(ruleName, plugin as Plugin);

0 commit comments

Comments
 (0)