eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
161 lines (160 loc) • 6.53 kB
JavaScript
"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/S6759/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 index_js_1 = require("../helpers/index.js");
const typescript_1 = __importDefault(require("typescript"));
const meta_js_1 = require("./meta.js");
exports.rule = {
meta: (0, index_js_1.generateMeta)(meta_js_1.meta, {
hasSuggestions: true,
messages: {
readOnlyProps: 'Mark the props of the component as read-only.',
readOnlyPropsFix: 'Mark the props as read-only',
},
}),
create(context) {
const services = context.sourceCode.parserServices;
if (!(0, index_js_1.isRequiredParserServices)(services)) {
return {};
}
const functionInfo = [];
return {
':function'() {
functionInfo.push({ returns: [] });
},
':function:exit'(node) {
/* Functional component */
const info = functionInfo.pop();
if (!info || !isFunctionalComponent(node, info)) {
return;
}
/* Provides props */
const [props] = node.params;
if (!props) {
return;
}
/* Includes type annotation */
const { typeAnnotation } = props;
if (!typeAnnotation) {
return;
}
/* Read-only props */
if (!isReadOnly(props, services)) {
context.report({
node: props,
messageId: 'readOnlyProps',
suggest: [
{
messageId: 'readOnlyPropsFix',
fix(fixer) {
const tpe = typeAnnotation.typeAnnotation;
const oldText = context.sourceCode.getText(tpe);
const newText = `Readonly<${oldText}>`;
return fixer.replaceText(tpe, newText);
},
},
],
});
}
},
ReturnStatement(node) {
(0, index_js_1.last)(functionInfo).returns.push(node);
},
};
/**
* A function is considered to be a React functional component if it
* is a named function declaration with a starting uppercase letter,
* it takes at most one parameter, and it returns some JSX value.
*/
function isFunctionalComponent(node, info) {
/* Named function declaration */
if (node.type !== 'FunctionDeclaration' || node.id === null) {
return false;
}
/* Starts with uppercase */
const name = node.id.name;
if (!(name && /^[A-Z]/.test(name))) {
return false;
}
/* At most one parameter (for props) */
const paramCount = node.params.length;
if (paramCount > 1) {
return false;
}
/* Returns JSX value */
const { returns } = info;
for (const ret of returns) {
if (!ret.argument) {
continue;
}
const value = (0, index_js_1.getUniqueWriteUsageOrNode)(context, ret.argument);
if (value.type.startsWith('JSX')) {
return true;
}
}
return false;
}
/**
* A props type is considered to be read-only if the type annotation
* is decorated with TypeScript utility type `Readonly` or if it refers
* to a pure type declaration, i.e. where all its members are read-only.
*/
function isReadOnly(props, services) {
const tpe = (0, index_js_1.getTypeFromTreeNode)(props, services);
/* Readonly utility type */
const { aliasSymbol } = tpe;
if (aliasSymbol?.escapedName === 'Readonly') {
return true;
}
/* Resolve symbol definition */
const symbol = tpe.getSymbol();
if (!symbol?.declarations) {
/* Kill the noise */
return true;
}
/* Pure type declaration */
const declarations = symbol.declarations;
for (const decl of declarations) {
if (typescript_1.default.isInterfaceDeclaration(decl)) {
const node = services.tsNodeToESTreeNodeMap.get(decl);
if (node?.type === 'TSInterfaceDeclaration') {
const { body: { body: members }, } = node;
if (members.every(m => m.type === 'TSPropertySignature' && m.readonly)) {
return true;
}
}
}
if (typescript_1.default.isTypeLiteralNode(decl)) {
const node = services.tsNodeToESTreeNodeMap.get(decl);
if (node?.type === 'TSTypeLiteral') {
const { members } = node;
if (members.every(m => m.type === 'TSPropertySignature' && m.readonly)) {
return true;
}
}
}
}
return false;
}
},
};