UNPKG

eslint-plugin-sonarjs

Version:
228 lines (227 loc) 10 kB
"use strict"; /* * 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; }, }; }