Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ jobs:
npm config set registry https://repox.jfrog.io/artifactory/api/npm/npm/
- if: steps.cache.outputs.cache-hit != 'true'
name: Install NPM dependencies
run: npm ci
run: npm ci --legacy-peer-deps

populate_npm_cache_win:
runs-on: github-windows-latest-s
Expand Down Expand Up @@ -300,6 +300,7 @@ jobs:
- name: Build ESLint plugin
run: npm run eslint-plugin:build
env:
NPM_CONFIG_LEGACY_PEER_DEPS: true
GITHUB_TOKEN: ${{ fromJSON(steps.rspec-secrets.outputs.vault).RSPEC_GITHUB_TOKEN }}
- name: Check README freshness
if: github.event_name == 'pull_request'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,7 @@ void test() {
.logOutput()
.stream()
.filter(l ->
l
.message()
.matches(
"Creating TypeScript(\\(\\d\\.\\d\\.\\d\\))? program with configuration file.*"
)
l.message().matches("Creating TypeScript(\\([^)]*\\))? program with configuration file.*")
)
).hasSize(2);
}
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
"stylelint": "17.4.0",
"stylelint-config-html": "1.1.0",
"ts-api-utils": "2.4.0",
"typescript": "5.9.3",
"typescript": "6.0.1-rc",
"vue-eslint-parser": "10.4.0",
"ws": "8.19.0",
"yaml": "2.8.2"
Expand Down
1 change: 1 addition & 0 deletions packages/jsts/src/program/tsconfig/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type CustomParseConfigHost = {
export const defaultCompilerOptions: ts.CompilerOptions = {
allowJs: true,
noImplicitAny: true,
strict: false,
};

/**
Expand Down
29 changes: 27 additions & 2 deletions packages/jsts/src/rules/S1154/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,38 @@
return variable;
}

function isFunctionLike(arg: estree.Expression | estree.SpreadElement | undefined) {

Check warning on line 53 in packages/jsts/src/rules/S1154/rule.ts

View check run for this annotation

SonarQube-Next / SonarQube Code Analysis

Move function 'isFunctionLike' to the outer scope.

[S7721] Functions should be moved to the highest possible scope See more on https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.javascript%3Ajavascript&pullRequest=6626&issues=74561e41-4a33-4b6c-97b6-59973053253b&open=74561e41-4a33-4b6c-97b6-59973053253b
return arg?.type === 'FunctionExpression' || arg?.type === 'ArrowFunctionExpression';
}

function isFunctionType(arg: estree.Expression | estree.SpreadElement | undefined) {
if (!arg || arg.type === 'SpreadElement') {
return false;
}
return getTypeFromTreeNode(arg, services).getCallSignatures().length > 0;
}

function isCallbackBasedReplacement(
property: estree.MemberExpression['property'],
args: Array<estree.Expression | estree.SpreadElement>,
) {
if (property.type !== 'Identifier' || !['replace', 'replaceAll'].includes(property.name)) {
return false;
}
const replacementArg = args[1];
return isFunctionLike(replacementArg) || isFunctionType(replacementArg);
}

return {
'ExpressionStatement > CallExpression[callee.type="MemberExpression"]': (
node: estree.Node,
) => {
const { object, property } = (node as estree.CallExpression)
.callee as estree.MemberExpression;
const callExpression = node as estree.CallExpression;
const { object, property } = callExpression.callee as estree.MemberExpression;
if (isString(object) && property.type === 'Identifier') {
if (isCallbackBasedReplacement(property, callExpression.arguments)) {
return;
}
context.report({
messageId: 'uselessStringOp',
data: {
Expand Down
15 changes: 15 additions & 0 deletions packages/jsts/src/rules/S1154/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ describe('S1154', () => {
{
code: `'hello'['whatever']();`,
},
{
code: `
'hello'.replace(/l/g, () => 'x');
`,
},
{
code: `
const replacer = (value: string) => value.toUpperCase();
'hello'.replace(/l/g, replacer);
`,
},
],
invalid: [
{
Expand Down Expand Up @@ -96,6 +107,10 @@ describe('S1154', () => {
code: `'hello'.substr(1, 2).toUpperCase();`,
errors: 1,
},
{
code: `'hello'.replace(/l/g, 'x');`,
errors: 1,
},
],
});
});
Expand Down
30 changes: 29 additions & 1 deletion packages/jsts/src/rules/S3403/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ export const rule: Rule.RuleModule = {
return {
BinaryExpression: (node: estree.Node) => {
const { left, operator, right } = node as estree.BinaryExpression;
if (['===', '!=='].includes(operator) && !isComparableTo(left, right)) {
if (
['===', '!=='].includes(operator) &&
!isDefensiveNullishCheck(left, right) &&
!isComparableTo(left, right)
) {
const [actual, expected, outcome] =
operator === '===' ? ['===', '==', 'false'] : ['!==', '!=', 'true'];
const operatorToken = context.sourceCode
Expand All @@ -79,6 +83,30 @@ export const rule: Rule.RuleModule = {
},
};

function isDefensiveNullishCheck(left: estree.Node, right: estree.Node) {
return (
(isNullishLiteral(left) && isReferenceLike(right)) ||
(isNullishLiteral(right) && isReferenceLike(left))
);
}

function isNullishLiteral(node: estree.Node) {
return (
(node.type === 'Literal' && node.value === null) ||
(node.type === 'Identifier' && node.name === 'undefined') ||
(node.type === 'UnaryExpression' && node.operator === 'void')
);
}

function isReferenceLike(node: estree.Node) {
return (
node.type === 'Identifier' ||
node.type === 'MemberExpression' ||
node.type === 'ChainExpression' ||
node.type === 'ThisExpression'
);
}

/**
* Checks if a type is indeterminate (its actual value cannot be determined at compile time).
* - Unknown: can be compared with anything
Expand Down
10 changes: 10 additions & 0 deletions packages/jsts/src/rules/S3403/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ describe('S3403', () => {
code: `
let str = 'str', obj = {};
str !== obj;`,
},
{
// Defensive check on indexed access can be intentional in JS/legacy patterns.
code: `
const items = ['a'];
if (items[0] === undefined) {
doSomething();
}
function doSomething() {}
`,
},
{
code: `
Expand Down
5 changes: 4 additions & 1 deletion packages/jsts/src/rules/S3757/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ export const rule: Rule.RuleModule = {

function isObjectType(...types: ts.Type[]): boolean {
return types.some(
t => !!(t.getFlags() & ts.TypeFlags.Object) && !isDate(t) && t.symbol?.name !== 'Number',
t =>
!!(t.getFlags() & ts.TypeFlags.Object) &&
!isDate(t) &&
!['Number', 'Boolean'].includes(t.symbol?.name ?? ''),
);
}

Expand Down
5 changes: 5 additions & 0 deletions packages/jsts/src/rules/S3757/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ describe('S3757', () => {
}
`,
},
{
code: `
new Boolean(true) - 1;
`,
},
],
invalid: [
{
Expand Down
15 changes: 15 additions & 0 deletions packages/jsts/src/rules/S6551/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export function decorate(rule: Rule.RuleModule): Rule.RuleModule {
meta: generateMeta(meta, rule.meta),
},
(context, reportDescriptor) => {
if (isTestLikeFile(context.physicalFilename || context.filename)) {
return;
}
if ('node' in reportDescriptor) {
const services = context.sourceCode.parserServices;
if (isGenericType(reportDescriptor.node as TSESTree.Node, services)) {
Expand All @@ -41,3 +44,15 @@ export function decorate(rule: Rule.RuleModule): Rule.RuleModule {
},
);
}

function isTestLikeFile(filename: string | undefined) {
if (!filename) {
return false;
}
return (
/(^|[\\/])__tests__([\\/]|$)/.test(filename) ||
/(^|[\\/])tests?([\\/]|$)/.test(filename) ||
/(?:^|[._-])test\.[cm]?[jt]sx?$/.test(filename) ||
/(?:^|[._-])spec\.[cm]?[jt]sx?$/.test(filename)
);
}
1 change: 1 addition & 0 deletions packages/jsts/src/rules/S6551/fixtures/r6551.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions packages/jsts/src/rules/S6551/fixtures/r6551.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
46 changes: 46 additions & 0 deletions packages/jsts/src/rules/S6551/unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2025 SonarSource Sàrl
* 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/
*/
import { rule } from './index.js';
import { RuleTester } from '../../../tests/tools/testers/rule-tester.js';
import { describe, it } from 'node:test';
import path from 'node:path';

describe('S6551', () => {
it('S6551', () => {
const ruleTester = new RuleTester();
const code = `
function maybeString(foo: string | {}) {
foo.toString();
}
`;
ruleTester.run('Objects and classes converted to strings should define toString', rule, {
valid: [
{
code,
filename: path.join(import.meta.dirname, 'fixtures', 'r6551.test.ts'),
},
],
invalid: [
{
code,
filename: path.join(import.meta.dirname, 'fixtures', 'r6551.ts'),
errors: 1,
},
],
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('SonarQube project analysis', () => {
// Verify it created a TypeScript program
expect(
consoleLogMock.calls.some(call =>
/Creating TypeScript\(\d+\.\d+\.\d+\) program/.test(call.arguments[0] as string),
/Creating TypeScript\([^)]+\) program/.test(call.arguments[0] as string),
),
).toBe(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"compilerOptions": {
"outDir": "./lib",
"lib": ["ESNext"],
"module": "commonjs"
"module": "commonjs",
"moduleResolution": "node10"
},
"include": ["file.ts"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"compilerOptions": {
"outDir": "./lib",
"lib": ["ESNext"],
"module": "esnext"
"module": "esnext",
"moduleResolution": "classic"
},
"include": ["file.ts"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"compilerOptions": {
"outDir": "./lib",
"lib": ["ESNext"],
"module": "nodenext"
"module": "nodenext",
"moduleResolution": "nodenext"
},
"include": ["file.ts"]
}
1 change: 1 addition & 0 deletions packages/jsts/tests/program/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ describe('factory', () => {
const options = program.getCompilerOptions();

expect(options.allowJs).toBe(defaultCompilerOptions.allowJs);
expect(options.strict).toBe(defaultCompilerOptions.strict);
});

it('should handle TypeScript syntax correctly', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/jsts/tests/program/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ describe('defaultCompilerOptions', () => {
it('should have expected default values', () => {
expect(defaultCompilerOptions.allowJs).toBe(true);
expect(defaultCompilerOptions.noImplicitAny).toBe(true);
expect(defaultCompilerOptions.strict).toBe(false);
expect(defaultCompilerOptions.lib).toBeUndefined();
});
});
Expand Down
8 changes: 7 additions & 1 deletion packages/jsts/tests/tools/testers/fixtures/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
"compilerOptions": {
"allowJs": true,
"noImplicitAny": true,
"strict": false,
"strictNullChecks": false,
"lib": ["ESNext", "DOM"],
},
"files": ["placeholder.tsx"]
"include": [
"*.ts",
"*.tsx",
"../../../../src/rules/S6551/fixtures/*.ts"
]
}
3 changes: 3 additions & 0 deletions tsconfig-plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "ES2021",
"rootDir": "packages/jsts/src/rules",
"module": "nodenext",
"moduleResolution": "nodenext",
"lib": ["ES2022", "dom"],
Expand All @@ -12,7 +13,9 @@
"esModuleInterop": true,
"skipLibCheck": true,
"useUnknownInCatchVariables": false,
"ignoreDeprecations": "6.0",
"forceConsistentCasingInFileNames": true,
"types": ["node", "jsx-ast-utils-x"],
"typeRoots": ["./node_modules/@types", "./typings"]
},
"include": ["packages/jsts/src/rules/plugin.ts"]
Expand Down
Loading