UNPKG

eslint-plugin-sonarjs

Version:
285 lines (284 loc) 11 kB
"use strict"; /* * SonarQube JavaScript Plugin * Copyright (C) 2011-2025 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 Sonar Source-Available License Version 1, as published by SonarSource SA. * * 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 Sonar Source-Available License for more details. * * You should have received a copy of the Sonar Source-Available License * along with this program; if not, see https://sonarsource.com/license/ssal/ */ // https://sonarsource.github.io/rspec/#/rspec/S5843/javascript var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = void 0; const regexpp = __importStar(require("@eslint-community/regexpp")); const index_js_1 = require("../helpers/index.js"); const ast_js_1 = require("../helpers/regex/ast.js"); const meta_js_1 = require("./meta.js"); const extract_js_1 = require("../helpers/regex/extract.js"); const range_js_1 = require("../helpers/regex/range.js"); const location_js_1 = require("../helpers/regex/location.js"); const DEFAULT_THRESHOLD = 20; exports.rule = { meta: (0, index_js_1.generateMeta)(meta_js_1.meta, { schema: meta_js_1.schema }, true), create(context) { const threshold = context.options[0]?.threshold ?? DEFAULT_THRESHOLD; const services = context.sourceCode.parserServices; const regexNodes = []; return { 'Literal[regex]:exit': (node) => { regexNodes.push(node); }, 'NewExpression:exit': (node) => { if ((0, ast_js_1.isRegExpConstructor)(node)) { regexNodes.push(node); } }, 'CallExpression:exit': (node) => { const callExpr = node; if ((0, index_js_1.isRequiredParserServices)(services) && (0, ast_js_1.isStringRegexMethodCall)(callExpr, services)) { regexNodes.push(callExpr.arguments[0]); } else if ((0, ast_js_1.isRegExpConstructor)(callExpr)) { regexNodes.push(callExpr); } }, 'Program:exit': () => { regexNodes.forEach(regexNode => checkRegexComplexity(regexNode, threshold, context)); }, }; }, }; function checkRegexComplexity(regexNode, threshold, context) { for (const regexParts of findRegexParts(regexNode, context)) { let complexity = 0; const secondaryLocations = []; for (const regexPart of regexParts) { const calculator = new ComplexityCalculator(regexPart, context); calculator.visit(); calculator.components.forEach(component => { secondaryLocations.push((0, index_js_1.toSecondaryLocation)(component.location, component.message)); }); complexity += calculator.complexity; } if (complexity > threshold) { (0, index_js_1.report)(context, { message: `Simplify this regular expression to reduce its complexity from ${complexity} to the ${threshold} allowed.`, node: regexParts[0], }, secondaryLocations, complexity - threshold); } } } function findRegexParts(node, context) { const finder = new RegexPartFinder(context); finder.find(node); return finder.parts; } class RegexPartFinder { constructor(context) { this.context = context; this.parts = []; this.handledIdentifiers = []; } find(node) { if ((0, ast_js_1.isRegExpConstructor)(node)) { this.find(node.arguments[0]); } else if ((0, index_js_1.isRegexLiteral)(node)) { this.parts.push([node]); } else if ((0, index_js_1.isStringLiteral)(node)) { this.parts.push([node]); } else if ((0, index_js_1.isStaticTemplateLiteral)(node)) { this.parts.push([node]); } else if ((0, index_js_1.isIdentifier)(node)) { if (!this.handledIdentifiers.includes(node)) { this.handledIdentifiers.push(node); const initializer = (0, index_js_1.getUniqueWriteUsage)(this.context, node.name, node); if (initializer) { this.find(initializer); } } } else if ((0, index_js_1.isBinaryPlus)(node)) { const literals = []; this.findInStringConcatenation(node.left, literals); this.findInStringConcatenation(node.right, literals); if (literals.length > 0) { this.parts.push(literals); } } } findInStringConcatenation(node, literals) { if ((0, index_js_1.isStringLiteral)(node)) { literals.push(node); } else if ((0, index_js_1.isBinaryPlus)(node)) { this.findInStringConcatenation(node.left, literals); this.findInStringConcatenation(node.right, literals); } else { this.find(node); } } } class ComplexityCalculator { constructor(regexPart, context) { this.regexPart = regexPart; this.context = context; this.nesting = 1; this.complexity = 0; this.components = []; this.regexPartAST = (0, extract_js_1.getParsedRegex)(regexPart, context); } visit() { if (!this.regexPartAST) { return; } regexpp.visitRegExpAST(this.regexPartAST, { onAssertionEnter: (node) => { /* lookaround */ if (node.kind === 'lookahead' || node.kind === 'lookbehind') { const [start, end] = (0, range_js_1.getRegexpRange)(this.regexPart, node); this.increaseComplexity(this.nesting, node, [ 0, -(end - start - 1) + (node.kind === 'lookahead' ? '?='.length : '?<='.length), ]); this.nesting++; this.onDisjunctionEnter(node); } }, onAssertionLeave: (node) => { /* lookaround */ if (node.kind === 'lookahead' || node.kind === 'lookbehind') { this.onDisjunctionLeave(node); this.nesting--; } }, onBackreferenceEnter: (node) => { this.increaseComplexity(1, node); }, onCapturingGroupEnter: (node) => { /* disjunction */ this.onDisjunctionEnter(node); }, onCapturingGroupLeave: (node) => { /* disjunction */ this.onDisjunctionLeave(node); }, onCharacterClassEnter: (node) => { /* character class */ const [start, end] = (0, range_js_1.getRegexpRange)(this.regexPart, node); this.increaseComplexity(1, node, [0, -(end - start - 1)]); this.nesting++; }, onCharacterClassLeave: (_node) => { /* character class */ this.nesting--; }, onGroupEnter: (node) => { /* disjunction */ this.onDisjunctionEnter(node); }, onGroupLeave: (node) => { /* disjunction */ this.onDisjunctionLeave(node); }, onPatternEnter: (node) => { /* disjunction */ this.onDisjunctionEnter(node); }, onPatternLeave: (node) => { /* disjunction */ this.onDisjunctionLeave(node); }, onQuantifierEnter: (node) => { /* repetition */ const [start] = (0, range_js_1.getRegexpRange)(this.regexPart, node); const [, end] = (0, range_js_1.getRegexpRange)(this.regexPart, node.element); this.increaseComplexity(this.nesting, node, [end - start, 0]); this.nesting++; }, onQuantifierLeave: (_node) => { /* repetition */ this.nesting--; }, }); } increaseComplexity(increment, node, offset) { this.complexity += increment; let message = '+' + increment; if (increment > 1) { message += ` (incl ${increment - 1} for nesting)`; } const loc = (0, location_js_1.getRegexpLocation)(this.regexPart, node, this.context, offset); if (loc) { this.components.push({ location: { loc, }, message, }); } } onDisjunctionEnter(node) { if (node.alternatives.length > 1) { let { alternatives } = node; let increment = this.nesting; while (alternatives.length > 1) { const [start, end] = (0, range_js_1.getRegexpRange)(this.regexPart, alternatives[1]); this.increaseComplexity(increment, alternatives[1], [-1, -(end - start)]); increment = 1; alternatives = alternatives.slice(1); } this.nesting++; } } onDisjunctionLeave(node) { if (node.alternatives.length > 1) { this.nesting--; } } }