Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 28 additions & 18 deletions packages/jsts/src/rules/S125/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import path from 'node:path';

const EXCLUDED_STATEMENTS = new Set(['BreakStatement', 'LabeledStatement', 'ContinueStatement']);

// Cheap prefilter: any meaningful JS statement must contain at least one of these characters,
// or be an import/export with a string literal (side-effect imports have no punctuation)
const CODE_CHAR_PATTERN = /[;{}()=<>]|\bimport\s+['"]|\bexport\s/;

const recognizer = new CodeRecognizer(0.9, new JavaScriptFootPrint());

interface GroupComment {
Expand Down Expand Up @@ -113,36 +117,38 @@ export const rule: Rule.RuleModule = {
},
};

function isExpressionExclusion(statement: estree.Node, code: SourceCode) {
function isExpressionExclusion(statement: estree.Node, value: string, program: AST.Program) {
if (statement.type === 'ExpressionStatement') {
const expression = statement.expression;
if (
expression.type === 'Identifier' ||
expression.type === 'SequenceExpression' ||
isUnaryPlusOrMinus(expression) ||
isExcludedLiteral(expression) ||
!code.getLastToken(statement, token => token.value === ';')
isExcludedLiteral(expression)
) {
return true;
}
// Only construct SourceCode when we need getLastToken
const code = new SourceCode(value, program);
return !code.getLastToken(statement, token => token.value === ';');
}
return false;
}

function isExclusion(parsedBody: Array<estree.Node>, code: SourceCode) {
function isExclusion(parsedBody: Array<estree.Node>, value: string, program: AST.Program) {
if (parsedBody.length === 1) {
const singleStatement = parsedBody[0];
return (
EXCLUDED_STATEMENTS.has(singleStatement.type) ||
isReturnThrowExclusion(singleStatement) ||
isExpressionExclusion(singleStatement, code)
isExpressionExclusion(singleStatement, value, program)
);
}
return false;
}

function containsCode(value: string, context: Rule.RuleContext) {
if (!couldBeJsCode(value) || !context.languageOptions.parser) {
if (!CODE_CHAR_PATTERN.test(value) || !couldBeJsCode(value) || !context.languageOptions.parser) {
return false;
}

Expand All @@ -158,28 +164,32 @@ function containsCode(value: string, context: Rule.RuleContext) {
context.languageOptions?.parserOptions?.parser ?? context.languageOptions?.parser;
const result =
'parse' in parser ? parser.parse(value, options) : parser.parseForESLint(value, options).ast;
const parseResult = new SourceCode(value, result as AST.Program);
return parseResult.ast.body.length > 0 && !isExclusion(parseResult.ast.body, parseResult);
const program = result as AST.Program;
return program.body.length > 0 && !isExclusion(program.body, value, program);
} catch {
return false;
}
}

function couldBeJsCode(input: string): boolean {
return recognizer.extractCodeLines(input.split('\n')).length > 0;
return input.split('\n').some(line => recognizer.recognition(line) >= recognizer.threshold);
}

function injectMissingBraces(value: string) {
const openCurlyBraceNum = (value.match(/{/g) ?? []).length;
const closeCurlyBraceNum = (value.match(/}/g) ?? []).length;
const missingBraces = openCurlyBraceNum - closeCurlyBraceNum;
if (missingBraces > 0) {
return value + '}'.repeat(missingBraces);
} else if (missingBraces < 0) {
return '{'.repeat(-missingBraces) + value;
} else {
return value;
let balance = 0;
for (let i = 0; i < value.length; i++) {
if (value[i] === '{') {
balance++;
} else if (value[i] === '}') {
balance--;
}
}
if (balance > 0) {
return value + '}'.repeat(balance);
} else if (balance < 0) {
return '{'.repeat(-balance) + value;
}
return value;
}

function getCommentLocation(nodes: TSESTree.Comment[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,20 @@
import Detector from '../Detector.js';

export default class ContainsDetector extends Detector {
strings: (string | RegExp)[];
patterns: RegExp[];

constructor(probability: number, ...strings: (string | RegExp)[]) {
super(probability);
this.strings = strings;
this.patterns = strings.map(str =>
typeof str === 'string' ? new RegExp(escapeRegex(str), 'g') : str,
);
}

scan(line: string): number {
const lineWithoutSpaces = line.replace(/\s+/, '');
let matchers = 0;
for (const str of this.strings) {
let regex = str;
if (typeof str === 'string') {
regex = new RegExp(escapeRegex(str), 'g');
}
matchers += (lineWithoutSpaces.match(regex) ?? []).length;
for (const pattern of this.patterns) {
matchers += (lineWithoutSpaces.match(pattern) ?? []).length;

Check warning on line 33 in packages/jsts/src/rules/helpers/recognizers/detectors/ContainsDetector.ts

View check run for this annotation

SonarQube-Next / SonarQube Code Analysis

Use the "RegExp.exec()" method instead.

[S6594] "RegExp.exec()" should be preferred over "String.match()" See more on https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.javascript%3Ajavascript&pullRequest=6340&issues=344cfa85-c69d-4d6d-a8ce-29ccf44bce7b&open=344cfa85-c69d-4d6d-a8ce-29ccf44bce7b
}
return matchers;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
import Detector from '../Detector.js';

const WHITESPACE = /\s/;

export default class EndWithDetector extends Detector {
endOfLines: string[];

Expand All @@ -32,14 +34,10 @@ export default class EndWithDetector extends Detector {
return 1;
}
}
if (!isWhitespace(char) && char !== '*' && char !== '/') {
if (!WHITESPACE.test(char) && char !== '*' && char !== '/') {
return 0;
}
}
return 0;
}
}

function isWhitespace(char: string): boolean {
return /\s/.test(char);
}
Loading