Skip to content

Commit

Permalink
Add custom implementation for S5362 ('function-calc-no-invalid')
Browse files Browse the repository at this point in the history
  • Loading branch information
yassin-kammoun-sonarsource committed Apr 7, 2022
1 parent d71231b commit c5e62e1
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 0 deletions.
2 changes: 2 additions & 0 deletions eslint-bridge/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions eslint-bridge/package.json
Expand Up @@ -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",
Expand All @@ -86,6 +87,7 @@
"postcss-html",
"postcss-less",
"postcss-scss",
"postcss-value-parser",
"regexpp",
"run-node",
"scslre",
Expand Down
2 changes: 2 additions & 0 deletions eslint-bridge/src/analyzer.ts
Expand Up @@ -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: [],
Expand Down Expand Up @@ -127,6 +128,7 @@ export function analyzeCss(input: CssAnalysisInput): Promise<AnalysisResponse> {
codeFilename: filePath,
configFile: stylelintConfig,
};
stylelint.rules[functionCalcNoInvalid.ruleName] = functionCalcNoInvalid.rule;
return stylelint
.lint(options)
.then(result => ({ issues: fromStylelintToSonarIssues(result.results, filePath) }));
Expand Down
120 changes: 120 additions & 0 deletions eslint-bridge/src/rules/stylelint/function-calc-no-invalid.ts
@@ -0,0 +1,120 @@
/*
* 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()) {
if (node.type === 'word') {
/* 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;
}
}
/* missing spaces surrounding operator */
for (let i = 1; i < node.value.length - 1; ++i) {
if (['+', '-'].includes(node.value[i])) {
const operator = node.value[i];
complain(`Expected space before "${operator}" operator`);
complain(`Expected space after "${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;
}
});
};
});
1 change: 1 addition & 0 deletions eslint-bridge/tests/analyzer.test.ts
Expand Up @@ -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');
Expand Down
215 changes: 215 additions & 0 deletions eslint-bridge/tests/rules/stylint/function-calc-no-invalid.test.ts
@@ -0,0 +1,215 @@
/*
* 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 missing spaces surrounding operator', async () => {
const {
results: [{ warnings }],
} = await stylelint.lint({
code: '.foo {width: calc(100%-80px);}',
config,
});
expect(warnings).toHaveLength(2);
expect(warnings[0].text).toBe('Expected space before "-" operator');
expect(warnings[1].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');
});

0 comments on commit c5e62e1

Please sign in to comment.