Skip to content

Commit

Permalink
Merge pull request #294 from 0Miles/dev/beta
Browse files Browse the repository at this point in the history
Add(ESLint): Class collision check
  • Loading branch information
0Miles authored Oct 25, 2023
2 parents f916e4b + 8079eb3 commit 4f52c51
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 79 deletions.
5 changes: 5 additions & 0 deletions examples/eslint/collision.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- eslint-disable @master/css/class-order -->
<div class="m:10 m:40 m:50 m:60 m:20 m:30:hover m:40@dark">
...
</div>

3 changes: 2 additions & 1 deletion packages/eslint-config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ module.exports = {
plugins: ['@master/css'],
rules: {
'@master/css/class-order': 'warn',
'@master/css/class-validation': 'error'
'@master/css/class-validation': 'error',
'@master/css/class-collision': 'warn'
},
parserOptions: {
ecmaFeatures: {
Expand Down
105 changes: 105 additions & 0 deletions packages/eslint-plugin/lib/rules/class-collision.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

const astUtil = require('../utils/ast')
const defineVisitors = require('../utils/define-visitors')
const resolveContext = require('../utils/resolve-context')
const { createValidRules } = require('@master/css-validator')
const CssTree = require('css-tree')

module.exports = {
meta: {
docs: {
description: 'Avoid declaring the identical CSS property repeatedly',
category: 'Stylistic Issues',
recommended: false,
url: 'https://beta.css.master.co/docs/code-linting#avoid-declaring-the-identical-css-property-repeatedly',
},
messages: {
collisionClass: '{{message}}',
},
fixable: 'code'
},
create: function (context) {
const { options, settings, config } = resolveContext(context)
const visitNode = (node, arg = null) => {
astUtil.parseNodeRecursive(
node,
arg,
(classNames, node, originalClassNamesValue, start, end) => {
const sourceCode = context.getSourceCode()
const sourceCodeLines = sourceCode.lines
const nodeStartLine = node.loc.start.line
const nodeEndLine = node.loc.end.line

const parsedRules = classNames
.map(x => createValidRules(x, { config }))
.map(rules => {
if (rules.length) {
const ruleAst = CssTree.parse(rules[0].text, { parseValue: false })
const ruleStyles = []
CssTree.walk(ruleAst, (cssNode) => {
if (cssNode.type === "Declaration") {
ruleStyles.push({
key: cssNode.property,
value: cssNode.value.value
})
}
})

return {
selector: Object.values(rules[0].vendorSuffixSelectors ?? {})?.[0]?.[0],
mediaToken: rules[0].media?.token,
styles: ruleStyles
}
}
return null
})

for (let i = 0; i < classNames.length ; i++) {
const className = classNames[i]
const parsedRule = parsedRules[i]
const conflicts = []

if (parsedRule && parsedRule.styles.length === 1) {
for (let j = 0; j < classNames.length; j++) {
const compareClassName = classNames[j]
const compareRule = parsedRules[j]
if (i !== j && compareRule && compareRule.styles.length === 1
&& parsedRule.selector == compareRule.selector
&& parsedRule.mediaToken == compareRule.mediaToken
&& parsedRule.styles[0].key == compareRule.styles[0].key
) {
conflicts.push(compareClassName)
}
}

if (conflicts.length > 0) {
const conflictClassNamesMsg = conflicts.map(x => `\`${x}\``).join(' and ')
let fixClassNames = originalClassNamesValue
for (const conflictClassName of conflicts){
const regexSafe = conflictClassName.replace(/(\\|\.|\(|\)|\[|\]|\{|\}|\+|\*|\?|\^|\$|\||\/)/g, '\\$1')
fixClassNames = fixClassNames.replace(new RegExp(`\\s+${regexSafe}|${regexSafe}\\s+`), '')
}

context.report({
loc: astUtil.findLoc(className, sourceCodeLines, nodeStartLine, nodeEndLine),
messageId: 'collisionClass',
data: {
message: `\`${className}\` applies the same CSS declarations as ${conflictClassNamesMsg}.
`,
},
fix: function (fixer) {
return fixer.replaceTextRange([start, end], fixClassNames)
}
})
}
}
}
},
false,
false,
settings.ignoredKeys
)
}
return defineVisitors({ context, options, settings, config }, visitNode)
},
}
3 changes: 3 additions & 0 deletions packages/eslint-plugin/lib/rules/class-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ module.exports = {
messageId: 'disallowTraditionalClass',
data: {
message: `Disallow a traditional class \`${className}\`.`,
},
fix: function (fixer) {
return fixer.replaceTextRange([start, end], '')
}
})
}
Expand Down
31 changes: 29 additions & 2 deletions packages/eslint-plugin/lib/utils/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ function parseNodeRecursive(rootNode, childNode, cb, skipConditional = false, is
// TODO allow vue non litteral
let originalClassNamesValue
let classNames

let start = null
let end = null
let prefix = ''
let suffix = ''

if (childNode === null) {
originalClassNamesValue = extractValueFromNode(rootNode);
({ classNames } = extractClassnamesFromValue(originalClassNamesValue))
Expand All @@ -77,13 +83,22 @@ function parseNodeRecursive(rootNode, childNode, cb, skipConditional = false, is
// Don't run for empty className
return
}
cb(classNames, rootNode)
const range = extractRangeFromNode(rootNode)
if (rootNode.type === 'TextAttribute') {
start = range[0]
end = range[1]
} else {
start = range[0] + 1
end = range[1] - 1
}
cb(classNames, rootNode, originalClassNamesValue, start, end, prefix, suffix)
} else if (childNode === undefined) {
// Ignore invalid child candidates (probably inside complex TemplateLiteral)
return
} else {
const forceIsolation = skipConditional ? true : isolate
let trim = false

switch (childNode.type) {
case 'TemplateLiteral':
childNode.expressions.forEach((exp) => {
Expand Down Expand Up @@ -135,9 +150,21 @@ function parseNodeRecursive(rootNode, childNode, cb, skipConditional = false, is
case 'Literal':
trim = true
originalClassNamesValue = childNode.value
start = childNode.range[0] + 1
end = childNode.range[1] - 1
break
case 'SvelteLiteral':
originalClassNamesValue = childNode.value
start = childNode.range[0]
end = childNode.range[1]
break
case 'TemplateElement':
originalClassNamesValue = childNode.value.raw
start = childNode.range[0]
end = childNode.range[1]
const txt = context.getSourceCode().getText(childNode)
prefix = astUtil.getTemplateElementPrefix(txt, originalClassNamesValue)
suffix = astUtil.getTemplateElementSuffix(txt, originalClassNamesValue)
break
}
({ classNames } = extractClassnamesFromValue(originalClassNamesValue))
Expand All @@ -147,7 +174,7 @@ function parseNodeRecursive(rootNode, childNode, cb, skipConditional = false, is
return
}
const targetNode = isolate ? null : rootNode
cb(classNames, targetNode)
cb(classNames, targetNode, originalClassNamesValue, start, end, prefix, suffix)
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dependencies": {
"@master/css": "workspace:^",
"@master/css-validator": "workspace:^",
"css-tree": "^2.3.1",
"explore-config": "^2.2.10",
"requireindex": "^1.2.0"
},
Expand Down
27 changes: 27 additions & 0 deletions packages/eslint-plugin/tests/collision.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

const rule = require('../lib/rules/class-collision')
const RuleTester = require('eslint').RuleTester

new RuleTester({
parserOptions: {
ecmaFeatures: {
jsx: true,
},
}
}).run('collision', rule, {
valid: [
{
code: `<div class="m:10 m:30:hover m:40@dark">Simple, basic</div>`,
}
],
invalid: [
{
code: `<div class="m:10 m:20 m:30:hover m:40@dark">collision</div>`,
output: `<div class="m:10 m:30:hover m:40@dark">collision</div>`,
errors: [
{ messageId: 'collisionClass' },
{ messageId: 'collisionClass' }
]
},
],
})
4 changes: 4 additions & 0 deletions packages/eslint-plugin/tests/order.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ new RuleTester({
{
code: `<div class="block my:1\u3000flex">Do not treat full width space as class separator</div>`,
},
{
code: `<div class="m:10 m:20 m:30:hover m:40@dark">Collision class</div>`,
},

],
invalid: [
{
Expand Down
Loading

0 comments on commit 4f52c51

Please sign in to comment.