|
1 |
| -import {createPlugin, utils} from 'stylelint'; |
| 1 | +import {createPlugin, utils, PostcssResult} from 'stylelint'; |
2 | 2 | import {basename} from 'path';
|
3 | 3 | import {AtRule, Declaration, Node} from 'postcss';
|
4 | 4 |
|
@@ -49,159 +49,183 @@ const plugin = createPlugin(ruleName, (isEnabled: boolean, _options, context) =>
|
49 | 49 | const args = matches[2].split(',').map(arg => arg.trim());
|
50 | 50 |
|
51 | 51 | if (type === 'theme') {
|
52 |
| - validateThemeMixin(node, args); |
| 52 | + validateThemeMixin(result, componentName, node, args, !!context.fix); |
53 | 53 | } else {
|
54 |
| - validateIndividualSystemMixins(node, type, args); |
| 54 | + validateIndividualSystemMixins(result, node, type, args, !!context.fix); |
55 | 55 | }
|
56 | 56 | });
|
| 57 | + }; |
| 58 | +}); |
57 | 59 |
|
58 |
| - function validateThemeMixin(node: AtRule, args: string[]) { |
59 |
| - if (args.length !== 1) { |
60 |
| - reportError(node, 'Expected theme mixin to only declare a single argument.'); |
61 |
| - } else if (args[0] !== '$theme-or-color-config') { |
62 |
| - if (context.fix) { |
63 |
| - node.params = node.params.replace(args[0], '$theme-or-color-config'); |
64 |
| - } else { |
65 |
| - reportError(node, 'Expected first mixin argument to be called `$theme-or-color-config`.'); |
66 |
| - } |
67 |
| - } |
68 |
| - |
69 |
| - const themePropName = `$theme`; |
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}')`, |
| 60 | +/** Validates a `theme` mixin. */ |
| 61 | +function validateThemeMixin( |
| 62 | + result: PostcssResult, |
| 63 | + componentName: string, |
| 64 | + node: AtRule, |
| 65 | + args: string[], |
| 66 | + shouldFix: boolean, |
| 67 | +) { |
| 68 | + if (args.length !== 1) { |
| 69 | + reportError(result, node, 'Expected theme mixin to only declare a single argument.'); |
| 70 | + } else if (args[0] !== '$theme-or-color-config') { |
| 71 | + if (shouldFix) { |
| 72 | + node.params = node.params.replace(args[0], '$theme-or-color-config'); |
| 73 | + } else { |
| 74 | + reportError( |
| 75 | + result, |
| 76 | + node, |
| 77 | + 'Expected first mixin argument to be called `$theme-or-color-config`.', |
75 | 78 | );
|
| 79 | + } |
| 80 | + } |
76 | 81 |
|
77 |
| - let legacyConfigDecl: Declaration | null = null; |
78 |
| - let duplicateStylesCheck: AtRule | null = null; |
79 |
| - let hasNodesOutsideDuplicationCheck = false; |
80 |
| - let isLegacyConfigRetrievalFirstStatement = false; |
81 |
| - |
82 |
| - if (node.nodes) { |
83 |
| - for (let i = 0; i < node.nodes.length; i++) { |
84 |
| - const childNode = node.nodes[i]; |
85 |
| - if (childNode.type === 'decl' && legacyColorExtractExpr.test(childNode.value)) { |
86 |
| - legacyConfigDecl = childNode; |
87 |
| - isLegacyConfigRetrievalFirstStatement = i === 0; |
88 |
| - } else if ( |
89 |
| - childNode.type === 'atrule' && |
90 |
| - childNode.name === 'include' && |
91 |
| - duplicateStylesCheckExpr.test(childNode.params) |
92 |
| - ) { |
93 |
| - duplicateStylesCheck = childNode; |
94 |
| - } else if (childNode.type !== 'comment') { |
95 |
| - hasNodesOutsideDuplicationCheck = true; |
96 |
| - } |
97 |
| - } |
98 |
| - } |
| 82 | + const themePropName = `$theme`; |
| 83 | + const legacyColorExtractExpr = anyPattern( |
| 84 | + `<..>.private-legacy-get-theme($theme-or-color-config)`, |
| 85 | + ); |
| 86 | + const duplicateStylesCheckExpr = anyPattern( |
| 87 | + `<..>.private-check-duplicate-theme-styles(${themePropName}, '${componentName}')`, |
| 88 | + ); |
99 | 89 |
|
100 |
| - if (!legacyConfigDecl) { |
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 |
| - } else if (legacyConfigDecl.prop !== themePropName) { |
109 |
| - reportError( |
110 |
| - legacyConfigDecl, |
111 |
| - `For consistency, theme variable should be called: ${themePropName}`, |
112 |
| - ); |
113 |
| - } |
| 90 | + let legacyConfigDecl: Declaration | null = null; |
| 91 | + let duplicateStylesCheck: AtRule | null = null; |
| 92 | + let hasNodesOutsideDuplicationCheck = false; |
| 93 | + let isLegacyConfigRetrievalFirstStatement = false; |
114 | 94 |
|
115 |
| - if (!duplicateStylesCheck) { |
116 |
| - reportError( |
117 |
| - node, |
118 |
| - `Missing check for duplicative theme styles. Please include the ` + |
119 |
| - `duplicate styles check mixin: ${duplicateStylesCheckExpr}`, |
120 |
| - ); |
| 95 | + if (node.nodes) { |
| 96 | + for (let i = 0; i < node.nodes.length; i++) { |
| 97 | + const childNode = node.nodes[i]; |
| 98 | + if (childNode.type === 'decl' && legacyColorExtractExpr.test(childNode.value)) { |
| 99 | + legacyConfigDecl = childNode; |
| 100 | + isLegacyConfigRetrievalFirstStatement = i === 0; |
| 101 | + } else if ( |
| 102 | + childNode.type === 'atrule' && |
| 103 | + childNode.name === 'include' && |
| 104 | + duplicateStylesCheckExpr.test(childNode.params) |
| 105 | + ) { |
| 106 | + duplicateStylesCheck = childNode; |
| 107 | + } else if (childNode.type !== 'comment') { |
| 108 | + hasNodesOutsideDuplicationCheck = true; |
121 | 109 | }
|
| 110 | + } |
| 111 | + } |
122 | 112 |
|
123 |
| - if (hasNodesOutsideDuplicationCheck) { |
124 |
| - reportError( |
125 |
| - node, |
126 |
| - `Expected nodes other than the "${legacyColorExtractExpr}" ` + |
127 |
| - `declaration to be nested inside the duplicate styles check.`, |
128 |
| - ); |
129 |
| - } |
| 113 | + if (!legacyConfigDecl) { |
| 114 | + reportError( |
| 115 | + result, |
| 116 | + node, |
| 117 | + `Legacy color API is not handled. Consumers could pass in a ` + |
| 118 | + `color configuration directly to the theme mixin. For backwards compatibility, ` + |
| 119 | + `use the following declaration to retrieve the theme object: ` + |
| 120 | + `${themePropName}: ${legacyColorExtractExpr}`, |
| 121 | + ); |
| 122 | + } else if (legacyConfigDecl.prop !== themePropName) { |
| 123 | + reportError( |
| 124 | + result, |
| 125 | + legacyConfigDecl, |
| 126 | + `For consistency, theme variable should be called: ${themePropName}`, |
| 127 | + ); |
| 128 | + } |
130 | 129 |
|
131 |
| - if (legacyConfigDecl !== null && !isLegacyConfigRetrievalFirstStatement) { |
132 |
| - reportError( |
133 |
| - legacyConfigDecl, |
134 |
| - 'Legacy configuration should be retrieved first in theme mixin.', |
135 |
| - ); |
136 |
| - } |
| 130 | + if (!duplicateStylesCheck) { |
| 131 | + reportError( |
| 132 | + result, |
| 133 | + node, |
| 134 | + `Missing check for duplicative theme styles. Please include the ` + |
| 135 | + `duplicate styles check mixin: ${duplicateStylesCheckExpr}`, |
| 136 | + ); |
| 137 | + } |
| 138 | + |
| 139 | + if (hasNodesOutsideDuplicationCheck) { |
| 140 | + reportError( |
| 141 | + result, |
| 142 | + node, |
| 143 | + `Expected nodes other than the "${legacyColorExtractExpr}" ` + |
| 144 | + `declaration to be nested inside the duplicate styles check.`, |
| 145 | + ); |
| 146 | + } |
| 147 | + |
| 148 | + if (legacyConfigDecl !== null && !isLegacyConfigRetrievalFirstStatement) { |
| 149 | + reportError( |
| 150 | + result, |
| 151 | + legacyConfigDecl, |
| 152 | + 'Legacy configuration should be retrieved first in theme mixin.', |
| 153 | + ); |
| 154 | + } |
| 155 | +} |
| 156 | + |
| 157 | +/** Validates one of the individual theming mixins (`color`, `typography` etc.) */ |
| 158 | +function validateIndividualSystemMixins( |
| 159 | + result: PostcssResult, |
| 160 | + node: AtRule, |
| 161 | + type: string, |
| 162 | + args: string[], |
| 163 | + shouldFix: boolean, |
| 164 | +) { |
| 165 | + if (args.length !== 1) { |
| 166 | + reportError(result, node, 'Expected mixin to only declare a single argument.'); |
| 167 | + } else if (args[0] !== '$config-or-theme') { |
| 168 | + if (shouldFix) { |
| 169 | + node.params = node.params.replace(args[0], '$config-or-theme'); |
| 170 | + } else { |
| 171 | + reportError(result, node, 'Expected first mixin argument to be called `$config-or-theme`.'); |
137 | 172 | }
|
| 173 | + } |
138 | 174 |
|
139 |
| - function validateIndividualSystemMixins(node: AtRule, type: string, args: string[]) { |
140 |
| - if (args.length !== 1) { |
141 |
| - reportError(node, 'Expected mixin to only declare a single argument.'); |
142 |
| - } else if (args[0] !== '$config-or-theme') { |
143 |
| - if (context.fix) { |
144 |
| - node.params = node.params.replace(args[0], '$config-or-theme'); |
145 |
| - } else { |
146 |
| - reportError(node, 'Expected first mixin argument to be called `$config-or-theme`.'); |
147 |
| - } |
148 |
| - } |
| 175 | + const expectedProperty = type === 'density' ? '$density-scale' : '$config'; |
| 176 | + const expectedValues = |
| 177 | + type === 'typography' |
| 178 | + ? [ |
| 179 | + anyPattern( |
| 180 | + '<..>.private-typography-to-2014-config(' + |
| 181 | + '<..>.get-typography-config($config-or-theme))', |
| 182 | + ), |
| 183 | + anyPattern( |
| 184 | + '<..>.private-typography-to-2018-config(' + |
| 185 | + '<..>.get-typography-config($config-or-theme))', |
| 186 | + ), |
| 187 | + ] |
| 188 | + : [anyPattern(`<..>.get-${type}-config($config-or-theme)`)]; |
| 189 | + let configExtractionNode: Declaration | null = null; |
| 190 | + let nonCommentNodeCount = 0; |
149 | 191 |
|
150 |
| - const expectedProperty = type === 'density' ? '$density-scale' : '$config'; |
151 |
| - const expectedValues = |
152 |
| - type === 'typography' |
153 |
| - ? [ |
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 |
| - ), |
162 |
| - ] |
163 |
| - : [anyPattern(`<..>.get-${type}-config($config-or-theme)`)]; |
164 |
| - let configExtractionNode: Declaration | null = null; |
165 |
| - let nonCommentNodeCount = 0; |
166 |
| - |
167 |
| - if (node.nodes) { |
168 |
| - for (const currentNode of node.nodes) { |
169 |
| - if (currentNode.type !== 'comment') { |
170 |
| - nonCommentNodeCount++; |
171 |
| - } |
172 |
| - |
173 |
| - if ( |
174 |
| - currentNode.type === 'decl' && |
175 |
| - expectedValues.some(v => v.test(stripNewlinesAndIndentation(currentNode.value))) |
176 |
| - ) { |
177 |
| - configExtractionNode = currentNode; |
178 |
| - break; |
179 |
| - } |
180 |
| - } |
| 192 | + if (node.nodes) { |
| 193 | + for (const currentNode of node.nodes) { |
| 194 | + if (currentNode.type !== 'comment') { |
| 195 | + nonCommentNodeCount++; |
181 | 196 | }
|
182 | 197 |
|
183 |
| - if (!configExtractionNode && nonCommentNodeCount > 0) { |
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 |
| - ); |
190 |
| - } else if (configExtractionNode && configExtractionNode.prop !== expectedProperty) { |
191 |
| - reportError( |
192 |
| - configExtractionNode, |
193 |
| - `For consistency, variable for configuration should ` + `be called: ${expectedProperty}`, |
194 |
| - ); |
| 198 | + if ( |
| 199 | + currentNode.type === 'decl' && |
| 200 | + expectedValues.some(v => v.test(stripNewlinesAndIndentation(currentNode.value))) |
| 201 | + ) { |
| 202 | + configExtractionNode = currentNode; |
| 203 | + break; |
195 | 204 | }
|
196 | 205 | }
|
| 206 | + } |
197 | 207 |
|
198 |
| - function reportError(node: Node, message: string) { |
199 |
| - // We need these `as any` casts, because Stylelint uses an older version |
200 |
| - // of the postcss typings that don't match up with our anymore. |
201 |
| - utils.report({result: result as any, ruleName, node: node, message}); |
202 |
| - } |
203 |
| - }; |
204 |
| -}); |
| 208 | + if (!configExtractionNode && nonCommentNodeCount > 0) { |
| 209 | + reportError( |
| 210 | + result, |
| 211 | + node, |
| 212 | + `Config is not extracted. Consumers could pass a theme object. ` + |
| 213 | + `Extract the configuration by using one of the following:` + |
| 214 | + expectedValues.map(expectedValue => `${expectedProperty}: ${expectedValue}`).join('\n'), |
| 215 | + ); |
| 216 | + } else if (configExtractionNode && configExtractionNode.prop !== expectedProperty) { |
| 217 | + reportError( |
| 218 | + result, |
| 219 | + configExtractionNode, |
| 220 | + `For consistency, variable for configuration should ` + `be called: ${expectedProperty}`, |
| 221 | + ); |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +/** Reports a lint error. */ |
| 226 | +function reportError(result: PostcssResult, node: Node, message: string) { |
| 227 | + utils.report({result, ruleName, node, message}); |
| 228 | +} |
205 | 229 |
|
206 | 230 | /** Figures out the name of the component from a file path. */
|
207 | 231 | function getComponentNameFromPath(filePath: string): string | null {
|
|
0 commit comments