eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
141 lines (140 loc) • 5.86 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/S3800/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 typescript_1 = __importDefault(require("typescript"));
const index_js_1 = require("../helpers/index.js");
const meta_js_1 = require("./meta.js");
class FunctionScope {
constructor() {
this.returnStatements = [];
}
getReturnStatements() {
return this.returnStatements.slice();
}
addReturnStatement(node) {
this.returnStatements.push(node);
}
}
const isASanitationFunction = (signature) => {
const { types } = signature.getReturnType();
return types.length === 2 && types.some(index_js_1.isBooleanTrueType) && types.some(index_js_1.isStringType);
};
exports.rule = {
meta: (0, index_js_1.generateMeta)(meta_js_1.meta, undefined, true),
create(context) {
let scopes = [];
const services = context.sourceCode.parserServices;
if (!(0, index_js_1.isRequiredParserServices)(services)) {
return {};
}
const checker = services.program.getTypeChecker();
function onFunctionExit(node) {
const returnStatements = scopes.pop().getReturnStatements();
if (returnStatements.some(retStmt => retStmt.argument?.type === 'ThisExpression')) {
return;
}
const signature = checker.getSignatureFromDeclaration(services.esTreeNodeToTSNodeMap.get(node));
if (signature && hasMultipleReturnTypes(signature, checker)) {
if (isASanitationFunction(signature)) {
return;
}
const stmts = returnStatements.filter(retStmt => !isNullLike((0, index_js_1.getTypeFromTreeNode)(retStmt.argument, services)));
const stmtsTypes = stmts.map(retStmt => (0, index_js_1.getTypeFromTreeNode)(retStmt.argument, services));
if (stmtsTypes.every(index_js_1.isAny)) {
return;
}
(0, index_js_1.report)(context, {
message: 'Refactor this function to always return the same type.',
loc: (0, index_js_1.getMainFunctionTokenLocation)(node, (0, index_js_1.getParent)(context, node), context),
}, stmts.map((stmt, i) => (0, index_js_1.toSecondaryLocation)(stmt, `Returns ${prettyPrint(stmtsTypes[i], checker)}`)));
}
}
return {
ReturnStatement: (node) => {
const retStmt = node;
if (scopes.length > 0 && retStmt.argument) {
scopes[scopes.length - 1].addReturnStatement(retStmt);
}
},
':function': () => {
scopes.push(new FunctionScope());
},
':function:exit': onFunctionExit,
'Program:exit': () => {
scopes = [];
},
};
},
};
function hasMultipleReturnTypes(signature, checker) {
const returnType = checker.getBaseTypeOfLiteralType(checker.getReturnTypeOfSignature(signature));
return isMixingTypes(returnType, checker) && !hasReturnTypeJSDoc(signature);
}
function isMixingTypes(type, checker) {
return (type.isUnion() &&
type.types
.filter(tp => !isNullLike(tp))
.map(tp => prettyPrint(tp, checker))
.filter(distinct).length > 1);
}
function hasReturnTypeJSDoc(signature) {
return signature.getJsDocTags().some(tag => ['return', 'returns'].includes(tag.name));
}
function isObjectLikeType(type) {
return !!(type.getFlags() & typescript_1.default.TypeFlags.Object);
}
function distinct(value, index, self) {
return self.indexOf(value) === index;
}
function prettyPrint(type, checker) {
if (type.isUnionOrIntersection()) {
const delimiter = type.isUnion() ? ' | ' : ' & ';
return type.types
.map(tp => prettyPrint(tp, checker))
.filter(distinct)
.join(delimiter);
}
const typeNode = checker.typeToTypeNode(type, undefined, undefined);
if (typeNode !== undefined) {
if (typescript_1.default.isFunctionTypeNode(typeNode)) {
return 'function';
}
if (typescript_1.default.isArrayTypeNode(typeNode) || isTypedArray(type, checker)) {
return 'array';
}
}
if (isObjectLikeType(type)) {
return 'object';
}
return checker.typeToString(checker.getBaseTypeOfLiteralType(type));
}
function isTypedArray(type, checker) {
const typeAsString = checker.typeToString(type);
// Since TS 5.7 typed arrays include the type of the elements in the string, eg. Float32Array<any>
return /.*Array(?:<[^>]*>)?$/.test(typeAsString);
}
function isNullLike(type) {
return ((type.flags & typescript_1.default.TypeFlags.Null) !== 0 ||
(type.flags & typescript_1.default.TypeFlags.Void) !== 0 ||
(type.flags & typescript_1.default.TypeFlags.Undefined) !== 0);
}
;