1
1
import { createPlugin , utils } from 'stylelint' ;
2
2
import { basename } from 'path' ;
3
- import { AtRule , atRule , decl , Declaration , Node } from 'postcss' ;
3
+ import { AtRule , Declaration , Node } from 'postcss' ;
4
4
5
5
/** Name of this stylelint rule. */
6
6
const ruleName = 'material/theme-mixin-api' ;
@@ -67,8 +67,12 @@ const plugin = createPlugin(ruleName, (isEnabled: boolean, _options, context) =>
67
67
}
68
68
69
69
const themePropName = `$theme` ;
70
- const legacyColorExtractExpr = `theming.private-legacy-get-theme($theme-or-color-config)` ;
71
- const duplicateStylesCheckExpr = `theming.private-check-duplicate-theme-styles(${ themePropName } , '${ componentName } ')` ;
70
+ const legacyColorExtractExpr = anyPattern (
71
+ `<..>.private-legacy-get-theme($theme-or-color-config)` ,
72
+ ) ;
73
+ const duplicateStylesCheckExpr = anyPattern (
74
+ `<..>.private-check-duplicate-theme-styles(${ themePropName } , '${ componentName } ')` ,
75
+ ) ;
72
76
73
77
let legacyConfigDecl : Declaration | null = null ;
74
78
let duplicateStylesCheck : AtRule | null = null ;
@@ -78,13 +82,13 @@ const plugin = createPlugin(ruleName, (isEnabled: boolean, _options, context) =>
78
82
if ( node . nodes ) {
79
83
for ( let i = 0 ; i < node . nodes . length ; i ++ ) {
80
84
const childNode = node . nodes [ i ] ;
81
- if ( childNode . type === 'decl' && childNode . value === legacyColorExtractExpr ) {
85
+ if ( childNode . type === 'decl' && legacyColorExtractExpr . test ( childNode . value ) ) {
82
86
legacyConfigDecl = childNode ;
83
87
isLegacyConfigRetrievalFirstStatement = i === 0 ;
84
88
} else if (
85
89
childNode . type === 'atrule' &&
86
90
childNode . name === 'include' &&
87
- childNode . params === duplicateStylesCheckExpr
91
+ duplicateStylesCheckExpr . test ( childNode . params )
88
92
) {
89
93
duplicateStylesCheck = childNode ;
90
94
} else if ( childNode . type !== 'comment' ) {
@@ -94,18 +98,13 @@ const plugin = createPlugin(ruleName, (isEnabled: boolean, _options, context) =>
94
98
}
95
99
96
100
if ( ! legacyConfigDecl ) {
97
- if ( context . fix ) {
98
- legacyConfigDecl = decl ( { prop : themePropName , value : legacyColorExtractExpr } ) ;
99
- node . insertBefore ( 0 , legacyConfigDecl ) ;
100
- } else {
101
- reportError (
102
- node ,
103
- `Legacy color API is not handled. Consumers could pass in a ` +
104
- `color configuration directly to the theme mixin. For backwards compatibility, ` +
105
- `use the following declaration to retrieve the theme object: ` +
106
- `${ themePropName } : ${ legacyColorExtractExpr } ` ,
107
- ) ;
108
- }
101
+ reportError (
102
+ node ,
103
+ `Legacy color API is not handled. Consumers could pass in a ` +
104
+ `color configuration directly to the theme mixin. For backwards compatibility, ` +
105
+ `use the following declaration to retrieve the theme object: ` +
106
+ `${ themePropName } : ${ legacyColorExtractExpr } ` ,
107
+ ) ;
109
108
} else if ( legacyConfigDecl . prop !== themePropName ) {
110
109
reportError (
111
110
legacyConfigDecl ,
@@ -114,16 +113,11 @@ const plugin = createPlugin(ruleName, (isEnabled: boolean, _options, context) =>
114
113
}
115
114
116
115
if ( ! duplicateStylesCheck ) {
117
- if ( context . fix ) {
118
- duplicateStylesCheck = atRule ( { name : 'include' , params : duplicateStylesCheckExpr } ) ;
119
- node . insertBefore ( 1 , duplicateStylesCheck ) ;
120
- } else {
121
- reportError (
122
- node ,
123
- `Missing check for duplicative theme styles. Please include the ` +
124
- `duplicate styles check mixin: ${ duplicateStylesCheckExpr } ` ,
125
- ) ;
126
- }
116
+ reportError (
117
+ node ,
118
+ `Missing check for duplicative theme styles. Please include the ` +
119
+ `duplicate styles check mixin: ${ duplicateStylesCheckExpr } ` ,
120
+ ) ;
127
121
}
128
122
129
123
if ( hasNodesOutsideDuplicationCheck ) {
@@ -157,12 +151,16 @@ const plugin = createPlugin(ruleName, (isEnabled: boolean, _options, context) =>
157
151
const expectedValues =
158
152
type === 'typography'
159
153
? [
160
- 'typography.private-typography-to-2014-config(' +
161
- 'theming.get-typography-config($config-or-theme))' ,
162
- 'typography.private-typography-to-2018-config(' +
163
- 'theming.get-typography-config($config-or-theme))' ,
154
+ anyPattern (
155
+ '<..>.private-typography-to-2014-config(' +
156
+ '<..>.get-typography-config($config-or-theme))' ,
157
+ ) ,
158
+ anyPattern (
159
+ '<..>.private-typography-to-2018-config(' +
160
+ '<..>.get-typography-config($config-or-theme))' ,
161
+ ) ,
164
162
]
165
- : [ `theming. get-${ type } -config($config-or-theme)`] ;
163
+ : [ anyPattern ( `<..>. get-${ type } -config($config-or-theme)`) ] ;
166
164
let configExtractionNode : Declaration | null = null ;
167
165
let nonCommentNodeCount = 0 ;
168
166
@@ -174,7 +172,7 @@ const plugin = createPlugin(ruleName, (isEnabled: boolean, _options, context) =>
174
172
175
173
if (
176
174
currentNode . type === 'decl' &&
177
- expectedValues . includes ( stripNewlinesAndIndentation ( currentNode . value ) )
175
+ expectedValues . some ( v => v . test ( stripNewlinesAndIndentation ( currentNode . value ) ) )
178
176
) {
179
177
configExtractionNode = currentNode ;
180
178
break ;
@@ -183,18 +181,12 @@ const plugin = createPlugin(ruleName, (isEnabled: boolean, _options, context) =>
183
181
}
184
182
185
183
if ( ! configExtractionNode && nonCommentNodeCount > 0 ) {
186
- if ( context . fix ) {
187
- node . insertBefore ( 0 , { prop : expectedProperty , value : expectedValues [ 0 ] } ) ;
188
- } else {
189
- reportError (
190
- node ,
191
- `Config is not extracted. Consumers could pass a theme object. ` +
192
- `Extract the configuration by using one of the following:` +
193
- expectedValues
194
- . map ( expectedValue => `${ expectedProperty } : ${ expectedValue } ` )
195
- . join ( '\n' ) ,
196
- ) ;
197
- }
184
+ reportError (
185
+ node ,
186
+ `Config is not extracted. Consumers could pass a theme object. ` +
187
+ `Extract the configuration by using one of the following:` +
188
+ expectedValues . map ( expectedValue => `${ expectedProperty } : ${ expectedValue } ` ) . join ( '\n' ) ,
189
+ ) ;
198
190
} else if ( configExtractionNode && configExtractionNode . prop !== expectedProperty ) {
199
191
reportError (
200
192
configExtractionNode ,
@@ -206,7 +198,7 @@ const plugin = createPlugin(ruleName, (isEnabled: boolean, _options, context) =>
206
198
function reportError ( node : Node , message : string ) {
207
199
// We need these `as any` casts, because Stylelint uses an older version
208
200
// of the postcss typings that don't match up with our anymore.
209
- utils . report ( { result : result as any , ruleName, node : node as any , message} ) ;
201
+ utils . report ( { result : result as any , ruleName, node : node , message} ) ;
210
202
}
211
203
} ;
212
204
} ) ;
@@ -235,4 +227,25 @@ function stripNewlinesAndIndentation(value: string): string {
235
227
return value . replace ( / ( \r | \n ) \s + / g, '' ) ;
236
228
}
237
229
230
+ /**
231
+ * Template string function that converts a pattern to a regular expression
232
+ * that can be used for assertions.
233
+ *
234
+ * The `<..>` character sequency is a placeholder that will allow for arbitrary
235
+ * content.
236
+ */
237
+ function anyPattern ( pattern ) : RegExp {
238
+ const regex = new RegExp (
239
+ `^${ sanitizeForRegularExpression ( pattern ) . replace ( / < \\ .\\ .> / g, '.*?' ) } $` ,
240
+ ) ;
241
+ // Preserve the original expression/pattern for better failure messages.
242
+ regex . toString = ( ) => pattern ;
243
+ return regex ;
244
+ }
245
+
246
+ /** Sanitizes a given string so that it can be used as literal in a RegExp. */
247
+ function sanitizeForRegularExpression ( value : string ) : string {
248
+ return value . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
249
+ }
250
+
238
251
export default plugin ;
0 commit comments