stylelint
Advanced tools
Comparing version 14.0.0 to 14.0.1
@@ -418,3 +418,3 @@ 'use strict'; | ||
if (micromatch.isMatch(filePath, filesGlobs)) { | ||
if (micromatch.isMatch(filePath, filesGlobs, { dot: true })) { | ||
config = mergeConfigs(config, configOverrides); | ||
@@ -421,0 +421,0 @@ } |
@@ -224,3 +224,3 @@ 'use strict'; | ||
*/ | ||
(el) => el.replace(/(\d+)\s+(\d+)/, (m, p1, p2) => dim(`${p1}:${p2}`)), | ||
(el) => el.replace(/(\d+)\s+(\d+)/, (_m, p1, p2) => dim(`${p1}:${p2}`)), | ||
) | ||
@@ -227,0 +227,0 @@ .join('\n'); |
@@ -96,6 +96,11 @@ 'use strict'; | ||
resolved = require(customSyntax); | ||
} catch { | ||
throw new Error( | ||
`Cannot resolve custom syntax module "${customSyntax}". Check that module "${customSyntax}" is available and spelled correctly.`, | ||
); | ||
} catch (error) { | ||
// @ts-expect-error -- TS2571: Object is of type 'unknown'. | ||
if (error && typeof error === 'object' && error.code === 'MODULE_NOT_FOUND') { | ||
throw new Error( | ||
`Cannot resolve custom syntax module "${customSyntax}". Check that module "${customSyntax}" is available and spelled correctly.`, | ||
); | ||
} | ||
throw error; | ||
} | ||
@@ -133,3 +138,3 @@ | ||
/** @type {{ [key: string]: string }} */ | ||
const previouslyInferedExtensions = { | ||
const previouslyInferredExtensions = { | ||
html: 'postcss-html', | ||
@@ -157,14 +162,8 @@ js: '@stylelint/postcss-css-in-js', | ||
function cssSyntax(stylelint, filePath) { | ||
const fileExtension = filePath | ||
? path | ||
.extname(filePath || '') | ||
.slice(1) | ||
.toLowerCase() | ||
: ''; | ||
const fileExtension = filePath ? path.extname(filePath).slice(1).toLowerCase() : ''; | ||
const extensions = ['css', 'pcss', 'postcss']; | ||
if (previouslyInferedExtensions[fileExtension]) { | ||
if (previouslyInferredExtensions[fileExtension]) { | ||
console.warn( | ||
`${filePath}: When linting something other than CSS, you should install an appropriate syntax, e.g. "${previouslyInferedExtensions[fileExtension]}", and use the "customSyntax" option`, | ||
`${filePath}: When linting something other than CSS, you should install an appropriate syntax, e.g. "${previouslyInferredExtensions[fileExtension]}", and use the "customSyntax" option`, | ||
); | ||
@@ -171,0 +170,0 @@ } |
'use strict'; | ||
const balancedMatch = require('balanced-match'); | ||
const isWhitespace = require('../../utils/isWhitespace'); | ||
const valueParser = require('postcss-value-parser'); | ||
const declarationValueIndex = require('../../utils/declarationValueIndex'); | ||
const getDeclarationValue = require('../../utils/getDeclarationValue'); | ||
const report = require('../../utils/report'); | ||
const ruleMessages = require('../../utils/ruleMessages'); | ||
const styleSearch = require('style-search'); | ||
const setDeclarationValue = require('../../utils/setDeclarationValue'); | ||
const validateOptions = require('../../utils/validateOptions'); | ||
const valueParser = require('postcss-value-parser'); | ||
@@ -19,3 +20,4 @@ const ruleName = 'function-calc-no-unspaced-operator'; | ||
/** @typedef {{ index: number, insert: boolean }} SymbolToFix */ | ||
const OPERATORS = new Set(['*', '/', '+', '-']); | ||
const OPERATOR_REGEX = /[*/+-]/; | ||
@@ -27,5 +29,3 @@ /** @type {import('stylelint').Rule} */ | ||
if (!validOptions) { | ||
return; | ||
} | ||
if (!validOptions) return; | ||
@@ -42,159 +42,259 @@ /** | ||
root.walkDecls((decl) => { | ||
/** @type {SymbolToFix[]} */ | ||
const symbolsToFix = []; | ||
let needsFix = false; | ||
const valueIndex = declarationValueIndex(decl); | ||
const parsedValue = valueParser(getDeclarationValue(decl)); | ||
valueParser(decl.value).walk((node) => { | ||
if (node.type !== 'function' || node.value.toLowerCase() !== 'calc') { | ||
return; | ||
/** | ||
* @param {import('postcss-value-parser').Node[]} nodes | ||
* @param {number} operatorIndex | ||
* @param {-1 | 1} direction | ||
*/ | ||
function checkAroundOperator(nodes, operatorIndex, direction) { | ||
const isBeforeOp = direction === -1; | ||
const currentNode = nodes[operatorIndex + direction]; | ||
const operator = nodes[operatorIndex].value; | ||
const operatorSourceIndex = nodes[operatorIndex].sourceIndex; | ||
if (currentNode && !isSingleSpace(currentNode)) { | ||
if (currentNode.type === 'word') { | ||
if (isBeforeOp) { | ||
const lastChar = currentNode.value.slice(-1); | ||
if (OPERATORS.has(lastChar)) { | ||
if (context.fix) { | ||
currentNode.value = `${currentNode.value.slice(0, -1)} ${lastChar}`; | ||
return true; | ||
} | ||
complain(messages.expectedOperatorBeforeSign(operator), decl, operatorSourceIndex); | ||
return true; | ||
} | ||
} else { | ||
const firstChar = currentNode.value.slice(0, 1); | ||
if (OPERATORS.has(firstChar)) { | ||
if (context.fix) { | ||
currentNode.value = `${firstChar} ${currentNode.value.slice(1)}`; | ||
return true; | ||
} | ||
complain(messages.expectedAfter(operator), decl, operatorSourceIndex); | ||
return true; | ||
} | ||
} | ||
if (context.fix) { | ||
needsFix = true; | ||
currentNode.value = isBeforeOp ? `${currentNode.value} ` : ` ${currentNode.value}`; | ||
return true; | ||
} | ||
complain( | ||
isBeforeOp ? messages.expectedBefore(operator) : messages.expectedAfter(operator), | ||
decl, | ||
valueIndex + operatorSourceIndex, | ||
); | ||
return true; | ||
} | ||
if (currentNode.type === 'space') { | ||
const indexOfFirstNewLine = currentNode.value.search(/(\n|\r\n)/); | ||
if (indexOfFirstNewLine === 0) return; | ||
if (context.fix) { | ||
needsFix = true; | ||
currentNode.value = | ||
indexOfFirstNewLine === -1 ? ' ' : currentNode.value.slice(indexOfFirstNewLine); | ||
return true; | ||
} | ||
const message = isBeforeOp | ||
? messages.expectedBefore(operator) | ||
: messages.expectedAfter(operator); | ||
complain(message, decl, valueIndex + operatorSourceIndex); | ||
return true; | ||
} | ||
if (currentNode.type === 'function') { | ||
if (context.fix) { | ||
needsFix = true; | ||
nodes.splice(operatorIndex, 0, { type: 'space', value: ' ', sourceIndex: 0 }); | ||
return true; | ||
} | ||
const message = isBeforeOp | ||
? messages.expectedBefore(operator) | ||
: messages.expectedAfter(operator); | ||
complain(message, decl, valueIndex + operatorSourceIndex); | ||
return true; | ||
} | ||
} | ||
const nodeText = valueParser.stringify(node); | ||
const parensMatch = balancedMatch('(', ')', nodeText); | ||
return false; | ||
} | ||
if (!parensMatch) { | ||
throw new Error(`No parens match: "${nodeText}"`); | ||
/** | ||
* @param {import('postcss-value-parser').Node[]} nodes | ||
*/ | ||
function checkForOperatorInFirstNode(nodes) { | ||
const firstNode = nodes[0]; | ||
const operatorIndex = | ||
(firstNode.type === 'word' || -1) && firstNode.value.search(OPERATOR_REGEX); | ||
const operator = firstNode.value.slice(operatorIndex, operatorIndex + 1); | ||
if (operatorIndex <= 0) return false; | ||
const charBefore = firstNode.value.charAt(operatorIndex - 1); | ||
const charAfter = firstNode.value.charAt(operatorIndex + 1); | ||
if (charBefore && charBefore !== ' ' && charAfter && charAfter !== ' ') { | ||
if (context.fix) { | ||
needsFix = true; | ||
firstNode.value = insertCharAtIndex(firstNode.value, operatorIndex + 1, ' '); | ||
firstNode.value = insertCharAtIndex(firstNode.value, operatorIndex, ' '); | ||
} else { | ||
complain( | ||
messages.expectedBefore(operator), | ||
decl, | ||
valueIndex + firstNode.sourceIndex + operatorIndex, | ||
); | ||
complain( | ||
messages.expectedAfter(operator), | ||
decl, | ||
valueIndex + firstNode.sourceIndex + operatorIndex + 1, | ||
); | ||
} | ||
} else if (charBefore && charBefore !== ' ') { | ||
if (context.fix) { | ||
needsFix = true; | ||
firstNode.value = insertCharAtIndex(firstNode.value, operatorIndex, ' '); | ||
} else { | ||
complain( | ||
messages.expectedBefore(operator), | ||
decl, | ||
valueIndex + firstNode.sourceIndex + operatorIndex, | ||
); | ||
} | ||
} else if (charAfter && charAfter !== ' ') { | ||
if (context.fix) { | ||
needsFix = true; | ||
firstNode.value = insertCharAtIndex(firstNode.value, operatorIndex, ' '); | ||
} else { | ||
complain( | ||
messages.expectedAfter(operator), | ||
decl, | ||
valueIndex + firstNode.sourceIndex + operatorIndex + 1, | ||
); | ||
} | ||
} | ||
if (decl.source == null || decl.source.start == null) { | ||
throw new Error('Declaration source must be present'); | ||
return true; | ||
} | ||
/** | ||
* @param {import('postcss-value-parser').Node[]} nodes | ||
*/ | ||
function checkForOperatorInLastNode(nodes) { | ||
if (nodes.length === 1) return false; | ||
const lastNode = nodes[nodes.length - 1]; | ||
const operatorIndex = | ||
(lastNode.type === 'word' || -1) && lastNode.value.search(OPERATOR_REGEX); | ||
if (lastNode.value[operatorIndex - 1] === ' ') return false; | ||
if (context.fix) { | ||
needsFix = true; | ||
lastNode.value = insertCharAtIndex(lastNode.value, operatorIndex + 1, ' ').trim(); | ||
lastNode.value = insertCharAtIndex(lastNode.value, operatorIndex, ' ').trim(); | ||
return true; | ||
} | ||
const rawExpression = parensMatch.body; | ||
const expressionIndex = | ||
decl.source.start.column + | ||
decl.prop.length + | ||
(decl.raws.between || '').length + | ||
node.sourceIndex; | ||
const expression = blurVariables(rawExpression); | ||
complain( | ||
messages.expectedOperatorBeforeSign(lastNode.value[operatorIndex]), | ||
decl, | ||
valueIndex + lastNode.sourceIndex + operatorIndex, | ||
); | ||
const parensMatchStart = parensMatch.start; | ||
return true; | ||
} | ||
checkSymbol('+'); | ||
checkSymbol('-'); | ||
checkSymbol('*'); | ||
checkSymbol('/'); | ||
/** | ||
* @param {import('postcss-value-parser').Node[]} nodes | ||
*/ | ||
function checkWords(nodes) { | ||
if (checkForOperatorInFirstNode(nodes)) return; | ||
/** | ||
* @param {string} symbol | ||
*/ | ||
function checkSymbol(symbol) { | ||
/** @type {import('style-search').Options} */ | ||
const styleSearchOptions = { | ||
source: expression, | ||
target: symbol, | ||
functionArguments: 'skip', | ||
}; | ||
if (checkForOperatorInLastNode(nodes)) return; | ||
styleSearch(styleSearchOptions, (match) => { | ||
const index = match.startIndex; | ||
const symbolIndex = node.sourceIndex + parensMatchStart + index + 1; | ||
nodes.forEach((node, index) => { | ||
const lastChar = node.value.slice(-1); | ||
const firstChar = node.value.slice(0, 1); | ||
// Deal with signs. | ||
// (@ and $ are considered "digits" here to allow for variable syntaxes | ||
// that permit signs in front of variables, e.g. `-$number`) | ||
// As is "." to deal with fractional numbers without a leading zero | ||
if ((symbol === '+' || symbol === '-') && /[\d@$.]/.test(expression[index + 1])) { | ||
const expressionBeforeSign = expression.slice(0, index); | ||
if (node.type === 'word') { | ||
if (index === 0 && OPERATORS.has(lastChar)) { | ||
if (context.fix) { | ||
node.value = `${node.value.slice(0, -1)} ${lastChar}`; | ||
// Ignore signs that directly follow a opening bracket | ||
if (expressionBeforeSign[expressionBeforeSign.length - 1] === '(') { | ||
return; | ||
} | ||
// Ignore signs at the beginning of the expression | ||
if (/^\s*$/.test(expressionBeforeSign)) { | ||
return; | ||
} | ||
complain(messages.expectedBefore(lastChar), decl, node.sourceIndex); | ||
} else if (index === nodes.length && OPERATORS.has(firstChar)) { | ||
if (context.fix) { | ||
node.value = `${firstChar} ${node.value.slice(1)}`; | ||
// Otherwise, ensure that there is a real operator preceding them | ||
if (/[*/+-]\s*$/.test(expressionBeforeSign)) { | ||
return; | ||
} | ||
if (!context.fix) { | ||
// And if not, complain | ||
complain( | ||
messages.expectedOperatorBeforeSign(symbol), | ||
decl, | ||
expressionIndex + index, | ||
); | ||
return; | ||
} | ||
complain(messages.expectedOperatorBeforeSign(firstChar), decl, node.sourceIndex); | ||
} | ||
} | ||
}); | ||
} | ||
const beforeOk = | ||
(expression[index - 1] === ' ' && !isWhitespace(expression[index - 2])) || | ||
newlineBefore(expression, index - 1); | ||
parsedValue.walk((node) => { | ||
if (node.type !== 'function' || node.value.toLowerCase() !== 'calc') return; | ||
if (!beforeOk) { | ||
if (context.fix) { | ||
let step = 1; | ||
let foundOperatorNode = false; | ||
// Remove all whitespace characters before the operator, e.g. `\t` | ||
while (isWhitespace(expression[index - step])) { | ||
symbolsToFix.push({ | ||
index: symbolIndex - step, | ||
insert: false, | ||
}); | ||
for (const [nodeIndex, currNode] of node.nodes.entries()) { | ||
if (currNode.type !== 'word' || !OPERATORS.has(currNode.value)) continue; | ||
step++; | ||
} | ||
foundOperatorNode = true; | ||
// Add only one space character | ||
symbolsToFix.push({ | ||
index: symbolIndex, | ||
insert: true, | ||
}); | ||
} else { | ||
complain(messages.expectedBefore(symbol), decl, expressionIndex + index); | ||
} | ||
} | ||
const nodeBefore = node.nodes[nodeIndex - 1]; | ||
const nodeAfter = node.nodes[nodeIndex + 1]; | ||
const afterOk = | ||
(expression[index + 1] === ' ' && !isWhitespace(expression[index + 2])) || | ||
isNewlineAtIndex(expression, index + 1); | ||
if (isSingleSpace(nodeBefore) && isSingleSpace(nodeAfter)) continue; | ||
if (!afterOk) { | ||
if (context.fix) { | ||
let step = 1; | ||
let spaceNeeded = true; | ||
if (checkAroundOperator(node.nodes, nodeIndex, 1)) continue; | ||
// Remove all whitespace characters before the operator or \n, e.g. \t | ||
while (isWhitespace(expression[index + step])) { | ||
if (isNewlineAtIndex(expression, index + step)) { | ||
spaceNeeded = false; | ||
break; | ||
} | ||
checkAroundOperator(node.nodes, nodeIndex, -1); | ||
} | ||
symbolsToFix.push({ | ||
index: symbolIndex + step, | ||
insert: false, | ||
}); | ||
step++; | ||
} | ||
// Insert one space character if there is no \n | ||
if (spaceNeeded) { | ||
symbolsToFix.push({ | ||
index: symbolIndex + 1, | ||
insert: true, | ||
}); | ||
} | ||
} else { | ||
complain(messages.expectedAfter(symbol), decl, expressionIndex + index); | ||
} | ||
} | ||
}); | ||
if (!foundOperatorNode) { | ||
checkWords(node.nodes); | ||
} | ||
}); | ||
if (context.fix) { | ||
decl.value = symbolsToFix.reduce((/** @type {string} */ fixedValue, { insert, index }) => { | ||
shiftIndexes(symbolsToFix, index, insert); | ||
return insert | ||
? insertCharAtIndex(fixedValue, index, ' ') | ||
: removeCharAtIndex(fixedValue, index); | ||
}, decl.value); | ||
if (needsFix) { | ||
setDeclarationValue(decl, parsedValue.toString()); | ||
} | ||
@@ -208,31 +308,2 @@ }); | ||
* @param {number} index | ||
*/ | ||
function isNewlineAtIndex(str, index) { | ||
return str[index] === '\n' || str.slice(index, index + 2) === '\r\n'; | ||
} | ||
/** | ||
* @param {SymbolToFix[]} symbolsToFix | ||
* @param {number} index | ||
* @param {boolean} insert | ||
*/ | ||
function shiftIndexes(symbolsToFix, index, insert) { | ||
symbolsToFix.forEach((symbol) => { | ||
if (symbol.index > index) { | ||
symbol.index += insert ? 1 : -1; | ||
} | ||
}); | ||
} | ||
/** | ||
* @param {string} str | ||
* @param {number} index | ||
*/ | ||
function removeCharAtIndex(str, index) { | ||
return str.slice(0, index) + str.slice(index + 1, str.length); | ||
} | ||
/** | ||
* @param {string} str | ||
* @param {number} index | ||
* @param {string} char | ||
@@ -245,26 +316,11 @@ */ | ||
/** | ||
* @param {string} source | ||
* @param {import('postcss-value-parser').Node} node | ||
* @returns {node is import('postcss-value-parser').SpaceNode & { value: ' ' } } | ||
*/ | ||
function blurVariables(source) { | ||
return source.replace(/[$@][^)\s]+|#\{.+?\}/g, '0'); | ||
function isSingleSpace(node) { | ||
return node && node.type === 'space' && node.value === ' '; | ||
} | ||
/** | ||
* @param {string} str | ||
* @param {number} startIndex | ||
*/ | ||
function newlineBefore(str, startIndex) { | ||
let index = startIndex; | ||
while (index && isWhitespace(str[index])) { | ||
if (str[index] === '\n') return true; | ||
index--; | ||
} | ||
return false; | ||
} | ||
rule.ruleName = ruleName; | ||
rule.messages = messages; | ||
module.exports = rule; |
@@ -9,5 +9,5 @@ 'use strict'; | ||
function isRectangular(areas) { | ||
return areas.every((row, i, arr) => row.length === arr[0].length); | ||
return areas.every((row, _i, arr) => row.length === arr[0].length); | ||
} | ||
module.exports = isRectangular; |
{ | ||
"name": "stylelint", | ||
"version": "14.0.0", | ||
"version": "14.0.1", | ||
"description": "A mighty, modern CSS linter.", | ||
@@ -26,15 +26,11 @@ "keywords": [ | ||
"main": "lib/index.js", | ||
"types": "types/stylelint/index.d.ts", | ||
"bin": { | ||
"stylelint": "bin/stylelint.js" | ||
}, | ||
"types": "types/stylelint/index.d.ts", | ||
"files": [ | ||
"bin", | ||
"CHANGELOG.md", | ||
"CONTRIBUTING.md", | ||
"SECURITY.md", | ||
"docs", | ||
"lib", | ||
"!**/__tests__", | ||
"!lib/testUtils", | ||
"bin/**/*.js", | ||
"lib/**/*.js", | ||
"!**/__tests__/**", | ||
"!lib/testUtils/**", | ||
"types/stylelint/index.d.ts" | ||
@@ -125,3 +121,3 @@ ], | ||
"global-modules": "^2.0.0", | ||
"globby": "^11.0.3", | ||
"globby": "^11.0.4", | ||
"globjoin": "^0.1.4", | ||
@@ -148,4 +144,4 @@ "html-tags": "^3.1.0", | ||
"specificity": "^0.4.1", | ||
"string-width": "^4.2.2", | ||
"strip-ansi": "^6.0.0", | ||
"string-width": "^4.2.3", | ||
"strip-ansi": "^6.0.1", | ||
"style-search": "^0.1.0", | ||
@@ -166,3 +162,2 @@ "svg-tags": "^1.0.0", | ||
"@types/imurmurhash": "^0.1.1", | ||
"@types/jest": "^27.0.2", | ||
"@types/micromatch": "^4.0.2", | ||
@@ -185,3 +180,3 @@ "@types/normalize-path": "^3.0.0", | ||
"jest-watch-typeahead": "^1.0.0", | ||
"lint-staged": "^11.2.3", | ||
"lint-staged": "^11.2.4", | ||
"np": "^7.5.0", | ||
@@ -188,0 +183,0 @@ "npm-run-all": "^4.1.5", |
@@ -27,3 +27,3 @@ # Stylelint | ||
![Example](https://github.com/stylelint/stylelint/raw/master/example.png?raw=true) | ||
![Example](https://github.com/stylelint/stylelint/raw/main/example.png?raw=true) | ||
@@ -95,2 +95,2 @@ ## Guides | ||
[The MIT License](https://raw.githubusercontent.com/stylelint/stylelint/master/LICENSE). | ||
[The MIT License](https://raw.githubusercontent.com/stylelint/stylelint/main/LICENSE). |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
36
24461
783057
356
Updatedglobby@^11.0.4
Updatedstring-width@^4.2.3
Updatedstrip-ansi@^6.0.1