eslint-plugin-sonarjs
Version:
228 lines (227 loc) • 10 kB
JavaScript
;
/*
* SonarQube JavaScript Plugin
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* You can redistribute and/or modify this program under the terms of
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
*
* 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/S4782/javascript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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 generate_meta_js_1 = require("../helpers/generate-meta.js");
const parser_services_js_1 = require("../helpers/parser-services.js");
const location_js_1 = require("../helpers/location.js");
const type_origin_js_1 = require("../helpers/type-origin.js");
const meta = __importStar(require("./generated-meta.js"));
exports.rule = {
meta: (0, generate_meta_js_1.generateMeta)(meta, { hasSuggestions: true }),
create(context) {
const parserServices = context.sourceCode.parserServices;
const hasTypeChecking = (0, parser_services_js_1.isRequiredParserServices)(parserServices);
const compilerOptions = hasTypeChecking
? parserServices.program.getCompilerOptions()
: undefined;
if (compilerOptions?.exactOptionalPropertyTypes) {
return {};
}
const canResolveTypes = hasTypeChecking
? (compilerOptions?.strictNullChecks ?? compilerOptions?.strict ?? false)
: false;
function checkProperty(node) {
const tsNode = node;
const optionalToken = context.sourceCode.getFirstToken(node, token => token.value === '?');
const rootType = tsNode.typeAnnotation?.typeAnnotation;
if (!tsNode.optional || !optionalToken || !rootType) {
return;
}
let typeNode;
if (rootType.type === 'TSUnionType') {
typeNode = findSyntacticUndefinedTypeNode(rootType);
}
if (!typeNode && canResolveTypes) {
typeNode = findSemanticUndefinedTypeNode(rootType, parserServices);
}
if (typeNode) {
const suggest = getQuickFixSuggestions(context, optionalToken, typeNode);
(0, location_js_1.report)(context, {
message: "Consider removing 'undefined' type or '?' specifier, one of them is redundant.",
loc: optionalToken.loc,
suggest,
}, [(0, location_js_1.toSecondaryLocation)(typeNode)]);
}
}
return {
'PropertyDefinition, TSPropertySignature': (node) => checkProperty(node),
};
},
};
function findSemanticUndefinedTypeNode(rootType, services) {
const tsTypeNode = services.esTreeNodeToTSNodeMap.get(rootType);
const checker = services.program.getTypeChecker();
const type = checker.getTypeAtLocation(tsTypeNode);
if (!type.isUnion()) {
return undefined;
}
const hasUndefined = type.types.some(isUndefinedType);
const hasAllUndefined = type.types.every(isUndefinedType);
if (!hasUndefined || hasAllUndefined) {
return undefined;
}
// If the user wrote `... | undefined` anywhere in the annotation along a
// path that propagates into the top-level resolved union (e.g. inside the
// type argument of a distributive external generic like Vue's `MaybeRef`),
// the inline keyword is editable — flag without consulting the classifier.
if (hasInlineUnionPositionUndefined(rootType, services)) {
return rootType;
}
// Otherwise `undefined` must come from a declaration: only flag when it's
// reachable from a user-editable (internal) top-level member. Pure external
// references like `React.ReactNode` stay suppressed.
const { internal } = (0, type_origin_js_1.classifyTypesByOrigin)(rootType, services);
const undefinedReachableFromInternal = internal.some(member => containsUndefined(member, services));
return undefinedReachableFromInternal ? rootType : undefined;
}
/**
* Looks for a `TSUndefinedKeyword` written in union position along a path
* that propagates into the top-level resolved union. We only descend through:
*
* - `TSUnionType.types` — unions are naturally distributive at every level.
* - `TSTypeReference.typeArguments.params`, but only when the reference's
* own resolved type is a union containing `undefined`. This filters out
* non-distributive wrappers like `Map<K, V | undefined>`, where the inner
* `| undefined` is buried inside the value type and cannot reach the
* property's top-level union.
*/
function hasInlineUnionPositionUndefined(root, services) {
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
if (node.type === 'TSUndefinedKeyword' && node.parent?.type === 'TSUnionType') {
return true;
}
pushPropagatingChildren(node, stack, services);
}
return false;
}
function pushPropagatingChildren(node, stack, services) {
if (node.type === 'TSUnionType') {
stack.push(...node.types);
return;
}
if (node.type === 'TSTypeReference' && propagatesUndefined(node, services)) {
const params = node.typeArguments?.params;
params && stack.push(...params);
}
}
function isUndefinedType(t) {
return (t.flags & typescript_1.default.TypeFlags.Undefined) !== 0;
}
function propagatesUndefined(node, services) {
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
const type = services.program.getTypeChecker().getTypeAtLocation(tsNode);
return type.isUnion() && type.types.some(isUndefinedType);
}
function containsUndefined(node, services) {
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
const type = services.program.getTypeChecker().getTypeAtLocation(tsNode);
const members = type.isUnion() ? type.types : [type];
return members.some(isUndefinedType);
}
function findSyntacticUndefinedTypeNode(typeNode) {
if (typeNode.type === 'TSUndefinedKeyword') {
return typeNode;
}
if (typeNode.type === 'TSUnionType') {
return typeNode.types.map(findSyntacticUndefinedTypeNode).find(type => type !== undefined);
}
return undefined;
}
function getQuickFixSuggestions(context, optionalToken, undefinedType) {
const suggestions = [
{
desc: 'Remove "?" operator',
fix: fixer => fixer.remove(optionalToken),
},
];
if (undefinedType.parent?.type === 'TSUnionType') {
suggestions.push(getUndefinedRemovalSuggestion(context, undefinedType));
}
return suggestions;
}
function getUndefinedRemovalSuggestion(context, undefinedType) {
return {
desc: 'Remove "undefined" type annotation',
fix: fixer => {
const fixes = [];
const unionType = undefinedType.parent;
if (unionType.types.length === 2) {
const unionTypeNode = unionType;
const otherType = unionType.types[0] === undefinedType ? unionType.types[1] : unionType.types[0];
const otherTypeText = context.sourceCode.getText(otherType);
fixes.push(fixer.replaceText(unionTypeNode, otherTypeText));
const tokenBefore = context.sourceCode.getTokenBefore(unionTypeNode);
const tokenAfter = context.sourceCode.getTokenAfter(unionTypeNode);
if (tokenBefore?.value === '(' && tokenAfter?.value === ')') {
fixes.push(fixer.remove(tokenBefore), fixer.remove(tokenAfter));
}
}
else {
const index = unionType.types.indexOf(undefinedType);
if (index === 0) {
fixes.push(fixer.removeRange([undefinedType.range[0], unionType.types[1].range[0]]));
}
else {
fixes.push(fixer.removeRange([unionType.types[index - 1].range[1], undefinedType.range[1]]));
}
}
return fixes;
},
};
}