eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
118 lines (117 loc) • 4.61 kB
JavaScript
/*
* 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/S4328/javascript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const builtin_modules_1 = __importDefault(require("builtin-modules"));
const typescript_1 = __importDefault(require("typescript"));
const index_js_1 = require("../helpers/index.js");
const meta_js_1 = require("./meta.js");
const messages = {
removeOrAddDependency: 'Either remove this import or add it as a dependency.',
};
exports.rule = {
meta: (0, index_js_1.generateMeta)(meta_js_1.meta, { messages, schema: meta_js_1.schema }),
create(context) {
// we need to find all the npm manifests from the directory of the analyzed file to the context working directory
const dependencies = (0, index_js_1.getDependencies)(context.filename, context.cwd);
const whitelist = context.options[0]?.whitelist || [];
const program = context.sourceCode.parserServices?.program;
let options, host;
if (program) {
options = program?.getCompilerOptions();
host = typescript_1.default.createCompilerHost(options);
}
return {
CallExpression: (node) => {
const call = node;
if (call.callee.type === 'Identifier' &&
call.callee.name === 'require' &&
call.arguments.length === 1) {
const [argument] = call.arguments;
if (argument.type === 'Literal') {
const requireToken = call.callee;
raiseOnImplicitImport(argument, requireToken.loc, dependencies, context.filename, host, options, whitelist, context);
}
}
},
ImportDeclaration: (node) => {
const module = node.source;
const importToken = context.sourceCode.getFirstToken(node);
raiseOnImplicitImport(module, importToken.loc, dependencies, context.filename, host, options, whitelist, context);
},
};
},
};
function raiseOnImplicitImport(module, loc, dependencies, filename, host, options, whitelist, context) {
const moduleName = module.value;
if (typeof moduleName !== 'string') {
return;
}
if (typescript_1.default.isExternalModuleNameRelative(moduleName)) {
return;
}
if (['node:', 'data:', 'file:'].some(prefix => moduleName.startsWith(prefix))) {
return;
}
const packageName = getPackageName(moduleName);
if (whitelist.includes(packageName)) {
return;
}
if (builtin_modules_1.default.includes(packageName)) {
return;
}
for (const dependency of dependencies) {
if (typeof dependency === 'string') {
if (dependency === packageName) {
return;
}
}
else if (dependency.match(moduleName)) {
//dependencies are globs for workspaces
return;
}
}
if (host && options) {
// check if Typescript can resolve path aliases and 'baseDir'-based import
const resolved = typescript_1.default.resolveModuleName(moduleName, filename, options, host);
if (resolved?.resolvedModule && !resolved.resolvedModule.isExternalLibraryImport) {
return;
}
}
context.report({
messageId: 'removeOrAddDependency',
loc,
});
}
function getPackageName(name) {
/*
- scoped `@namespace/foo/bar` -> package `@namespace/foo`
- scope `foo/bar` -> package `foo`
*/
const parts = name.split('/');
if (!name.startsWith('@')) {
return parts[0];
}
else {
return `${parts[0]}/${parts[1]}`;
}
}
;