From 514382088bee0f725e04160f97882741838f28d8 Mon Sep 17 00:00:00 2001 From: yassin-kammoun-sonarsource Date: Wed, 6 Apr 2022 13:03:54 +0200 Subject: [PATCH] Add custom implementation for S5362 ('function-calc-no-invalid') --- eslint-bridge/package-lock.json | 2 + eslint-bridge/package.json | 2 + eslint-bridge/src/analyzer.ts | 2 + .../stylelint/function-calc-no-invalid.ts | 109 ++++++++++ eslint-bridge/tests/analyzer.test.ts | 1 + .../stylint/function-calc-no-invalid.test.ts | 203 ++++++++++++++++++ 6 files changed, 319 insertions(+) create mode 100644 eslint-bridge/src/rules/stylelint/function-calc-no-invalid.ts create mode 100644 eslint-bridge/tests/rules/stylint/function-calc-no-invalid.test.ts diff --git a/eslint-bridge/package-lock.json b/eslint-bridge/package-lock.json index e03c0ace12d..b334ce9451a 100644 --- a/eslint-bridge/package-lock.json +++ b/eslint-bridge/package-lock.json @@ -26,6 +26,7 @@ "postcss-html", "postcss-less", "postcss-scss", + "postcss-value-parser", "regexpp", "run-node", "scslre", @@ -53,6 +54,7 @@ "postcss-html": "1.3.0", "postcss-less": "6.0.0", "postcss-scss": "4.0.3", + "postcss-value-parser": "4.2.0", "regexpp": "3.2.0", "run-node": "2.0.0", "scslre": "0.1.6", diff --git a/eslint-bridge/package.json b/eslint-bridge/package.json index de44b46d59f..1942a6a550e 100644 --- a/eslint-bridge/package.json +++ b/eslint-bridge/package.json @@ -60,6 +60,7 @@ "postcss-html": "1.3.0", "postcss-less": "6.0.0", "postcss-scss": "4.0.3", + "postcss-value-parser": "4.2.0", "regexpp": "3.2.0", "run-node": "2.0.0", "scslre": "0.1.6", @@ -86,6 +87,7 @@ "postcss-html", "postcss-less", "postcss-scss", + "postcss-value-parser", "regexpp", "run-node", "scslre", diff --git a/eslint-bridge/src/analyzer.ts b/eslint-bridge/src/analyzer.ts index 01c58856353..2d1369fb312 100644 --- a/eslint-bridge/src/analyzer.ts +++ b/eslint-bridge/src/analyzer.ts @@ -28,6 +28,7 @@ import { getContext } from './context'; import { hrtime } from 'process'; import * as stylelint from 'stylelint'; import { QuickFix } from './quickfix'; +import { rule as functionCalcNoInvalid } from './rules/stylelint/function-calc-no-invalid'; export const EMPTY_RESPONSE: AnalysisResponse = { issues: [], @@ -127,6 +128,7 @@ export function analyzeCss(input: CssAnalysisInput): Promise { codeFilename: filePath, configFile: stylelintConfig, }; + stylelint.rules[functionCalcNoInvalid.ruleName] = functionCalcNoInvalid.rule; return stylelint .lint(options) .then(result => ({ issues: fromStylelintToSonarIssues(result.results, filePath) })); diff --git a/eslint-bridge/src/rules/stylelint/function-calc-no-invalid.ts b/eslint-bridge/src/rules/stylelint/function-calc-no-invalid.ts new file mode 100644 index 00000000000..5614723049c --- /dev/null +++ b/eslint-bridge/src/rules/stylelint/function-calc-no-invalid.ts @@ -0,0 +1,109 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// https://sonarsource.github.io/rspec/#/rspec/S5362/css +import * as stylelint from 'stylelint'; +import postcssValueParser from 'postcss-value-parser'; + +const ruleName = 'function-calc-no-invalid'; +const operators = ['+', '-', '*', '/']; + +export const rule = stylelint.createPlugin(ruleName, function (_primaryOption, _secondaryOptions) { + return (root, result) => { + root.walkDecls(decl => { + /* flag to report an invalid expression iff the calc argument has no other issues */ + let complained = false; + postcssValueParser(decl.value).walk(calc => { + if (calc.type !== 'function' || calc.value.toLowerCase() !== 'calc') { + return; + } + const nodes = calc.nodes.filter(node => !isSpaceOrComment(node)); + for (const [index, node] of nodes.entries()) { + /* division by zero */ + if (node.value === '/') { + const operand = nodes[index + 1]; + if (operand && isZero(operand)) { + complain('Unexpected division by zero'); + complained = true; + } + } + /* missing space after operator */ + if (['+', '-'].includes(node.value[0]) && node.value.length > 1) { + const previous = nodes[index - 1]; + if (previous && !isOperator(previous)) { + const operator = node.value[0]; + complain(`Expected space after "${operator}" operator`); + complained = true; + } + } + /* missing space before operator */ + if (['+', '-'].includes(node.value[node.value.length - 1]) && node.value.length > 1) { + const after = nodes[index + 1]; + if (after && !isOperator(after)) { + const operator = node.value[node.value.length - 1]; + complain(`Expected space before "${operator}" operator`); + complained = true; + } + } + } + /* invalid expression */ + if (!complained && !isValid(nodes)) { + complain('Expected a valid expression'); + } + }); + + function isValid(nodes: postcssValueParser.Node[]) { + /* empty expression */ + if (nodes.length === 0) { + return false; + } + /* missing operator */ + for (let index = 1; index < nodes.length; index += 2) { + const node = nodes[index]; + if (!isOperator(node)) { + return false; + } + } + return true; + } + + function isSpaceOrComment(node: postcssValueParser.Node) { + return node.type === 'space' || node.type === 'comment'; + } + + function isOperator(node: postcssValueParser.Node) { + return node.type === 'word' && operators.includes(node.value); + } + + function isZero(node: postcssValueParser.Node) { + return node.type === 'word' && parseFloat(node.value) === 0; + } + + function complain(message: string) { + stylelint.utils.report({ + ruleName, + result, + message, + node: decl, + }); + complained = true; + } + }); + }; +}); diff --git a/eslint-bridge/tests/analyzer.test.ts b/eslint-bridge/tests/analyzer.test.ts index 78c43b48220..d7ad4444296 100644 --- a/eslint-bridge/tests/analyzer.test.ts +++ b/eslint-bridge/tests/analyzer.test.ts @@ -442,6 +442,7 @@ describe('#analyzeTypeScript', () => { }); jest.mock('stylelint'); +jest.mock('rules/stylelint/function-calc-no-invalid', () => ({ rule: { ruleName: '', rule: {} } })); describe('#analyzeCss', () => { const filePath = join(__dirname, 'fixtures', 'css', 'file.css'); diff --git a/eslint-bridge/tests/rules/stylint/function-calc-no-invalid.test.ts b/eslint-bridge/tests/rules/stylint/function-calc-no-invalid.test.ts new file mode 100644 index 00000000000..c37c32e45dd --- /dev/null +++ b/eslint-bridge/tests/rules/stylint/function-calc-no-invalid.test.ts @@ -0,0 +1,203 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as stylelint from 'stylelint'; +import { rule } from 'rules/stylelint/function-calc-no-invalid'; + +const config = { rules: { [rule.ruleName]: true } }; + +beforeAll(() => { + stylelint.rules[rule.ruleName] = rule.rule; +}); + +it('accepts single expression', async () => { + const { + results: [{ parseErrors, warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100%);}', + config, + }); + expect(parseErrors).toHaveLength(0); + expect(warnings).toHaveLength(0); +}); + +it('accepts compound expression', async () => { + const { + results: [{ parseErrors, warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% - 80px + 60pt);}', + config, + }); + expect(parseErrors).toHaveLength(0); + expect(warnings).toHaveLength(0); +}); + +it('accepts missing space before non-sign operator', async () => { + const { + results: [{ parseErrors, warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100%* 80px);}', + config, + }); + expect(parseErrors).toHaveLength(0); + expect(warnings).toHaveLength(0); +}); + +it('accepts missing space after non-sign operator', async () => { + const { + results: [{ parseErrors, warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% /1);}', + config, + }); + expect(parseErrors).toHaveLength(0); + expect(warnings).toHaveLength(0); +}); + +it('accepts division by 1', async () => { + const { + results: [{ parseErrors, warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% / 1);}', + config, + }); + expect(parseErrors).toHaveLength(0); + expect(warnings).toHaveLength(0); +}); + +it('accepts division by 0.1', async () => { + const { + results: [{ parseErrors, warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% / 0.1);}', + config, + }); + expect(parseErrors).toHaveLength(0); + expect(warnings).toHaveLength(0); +}); + +it('accepts division by 1px', async () => { + const { + results: [{ parseErrors, warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% / 1px);}', + config, + }); + expect(parseErrors).toHaveLength(0); + expect(warnings).toHaveLength(0); +}); + +it('rejects empty expression', async () => { + const { + results: [{ warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc();}', + config, + }); + const [{ text }] = warnings; + expect(text).toBe('Expected a valid expression'); +}); + +it('rejects space-only expression', async () => { + const { + results: [{ warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc( );}', + config, + }); + const [{ text }] = warnings; + expect(text).toBe('Expected a valid expression'); +}); + +it('rejects comment-only expression', async () => { + const { + results: [{ warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(/* this a comment */);}', + config, + }); + const [{ text }] = warnings; + expect(text).toBe('Expected a valid expression'); +}); + +it('rejects missing operator', async () => { + const { + results: [{ warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% 80px);}', + config, + }); + const [{ text }] = warnings; + expect(text).toBe('Expected a valid expression'); +}); + +it('rejects missing space before operator', async () => { + const { + results: [{ warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100%- 80px);}', + config, + }); + const [{ text }] = warnings; + expect(text).toBe('Expected space before "-" operator'); +}); + +it('rejects missing space after operator', async () => { + const { + results: [{ warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% -80px);}', + config, + }); + const [{ text }] = warnings; + expect(text).toBe('Expected space after "-" operator'); +}); + +it('rejects division by 0', async () => { + const { + results: [{ warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% / 0);}', + config, + }); + const [{ text }] = warnings; + expect(text).toBe('Unexpected division by zero'); +}); + +it('rejects division by 0.0', async () => { + const { + results: [{ warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% / 0.0);}', + config, + }); + const [{ text }] = warnings; + expect(text).toBe('Unexpected division by zero'); +}); + +it('rejects division by 0px', async () => { + const { + results: [{ warnings }], + } = await stylelint.lint({ + code: '.foo {width: calc(100% / 0px);}', + config, + }); + const [{ text }] = warnings; + expect(text).toBe('Unexpected division by zero'); +});