eslint-plugin-sonarjs
Version:
147 lines (146 loc) • 6.65 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.classifyTypesByOrigin = classifyTypesByOrigin;
/*
* 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/
*/
const typescript_1 = __importDefault(require("typescript"));
/**
* Partitions the syntactic members of a type annotation into "internal"
* (user-writable in this project) and "external" (declared in node_modules
* or in the TypeScript default lib).
*
* Operates on the syntactic AST node, not on a resolved `ts.Type`. Returning
* the original AST nodes lets callers produce locations, suggestions, or
* fixes targeting exactly what the user wrote.
*
* Classification rules per top-level member:
* - Keyword / literal types -> internal (the user wrote them directly).
* - TSTypeReference -> resolves the type name to a symbol and inspects its
* declarations.
* - TSIndexedAccessType with a literal property name -> resolves the accessed
* property and inspects its declarations. When no named property matches,
* the access goes through an index signature and the signature's own
* declaration is used instead (falling back to the declaration of the
* indexed type itself for signatures the checker synthesizes, such as
* `Record<K, V>`).
* - Any other composite constructor (TSIntersectionType, TSArrayType,
* TSTypeLiteral, TSConditionalType, ...) -> internal at the top level. We
* do not recurse; callers can if they need to.
*
* A member is external only when ALL of the declarations found for it live in
* files that satisfy `isSourceFileFromExternalLibrary` or
* `isSourceFileDefaultLibrary`. Any local declaration (declaration-merging
* escape hatch) makes the member internal, and so does a member whose
* declarations cannot be resolved at all — being reportable is the safe
* default, since callers use this to decide whether a fix is applicable.
*
* Known limitations:
* - Alias chains are not followed. If the user re-aliases an external type
* locally (e.g. `type Inner = ReactNode`), the local alias is internal
* because the user has a place to edit.
* - An indexed access with a non-literal index (`Ext[Key]`) is internal: the
* property cannot be pinned down syntactically.
* - A property inherited from an external base is external even though the
* local type could redeclare it, because the declaration the user would
* have to change is the external one.
*/
function classifyTypesByOrigin(typeNode, services) {
const members = typeNode.type === 'TSUnionType' ? typeNode.types : [typeNode];
const result = { internal: [], external: [] };
for (const member of members) {
if (isExternalMember(member, services)) {
result.external.push(member);
}
else {
result.internal.push(member);
}
}
return result;
}
function isExternalMember(member, services) {
const checker = services.program.getTypeChecker();
const tsNode = services.esTreeNodeToTSNodeMap.get(member);
let declarations;
if (member.type === 'TSTypeReference' && typescript_1.default.isTypeReferenceNode(tsNode)) {
declarations = declarationsOfSymbol(checker.getSymbolAtLocation(tsNode.typeName), checker);
}
else if (member.type === 'TSIndexedAccessType' && typescript_1.default.isIndexedAccessTypeNode(tsNode)) {
declarations = indexedAccessDeclarations(tsNode, checker);
}
else {
return false;
}
if (!declarations?.length) {
return false;
}
const program = services.program;
return declarations.every(decl => {
const sourceFile = decl.getSourceFile();
return (program.isSourceFileFromExternalLibrary(sourceFile) ||
program.isSourceFileDefaultLibrary(sourceFile));
});
}
function declarationsOfSymbol(symbol, checker) {
// Imported names resolve to a local alias symbol pointing at the import
// statement; without following the alias, external imports would look local.
if (symbol && symbol.flags & typescript_1.default.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(symbol);
}
return symbol?.declarations;
}
function indexedAccessDeclarations(node, checker) {
if (!typescript_1.default.isLiteralTypeNode(node.indexType)) {
return undefined;
}
const { literal } = node.indexType;
if (!typescript_1.default.isStringLiteral(literal) && !typescript_1.default.isNumericLiteral(literal)) {
return undefined;
}
const objectType = checker.getTypeAtLocation(node.objectType);
const property = checker.getPropertyOfType(objectType, literal.text);
if (property) {
return declarationsOfSymbol(property, checker);
}
return indexSignatureDeclarations(objectType, literal, checker);
}
/**
* Declarations backing an access that resolves through an index signature
* rather than a named property, e.g. `ExternalRecord['anyKey']`. A numeric
* literal can also hit a string signature, hence the two lookups.
*
* Mapped types (`Record<K, V>` and friends) have an index signature the
* checker synthesizes, with no declaration node to attribute. There we fall
* back to whatever declares the indexed type itself, which is the file the
* user would have to edit anyway.
*/
function indexSignatureDeclarations(objectType, literal, checker) {
const kinds = typescript_1.default.isNumericLiteral(literal)
? [typescript_1.default.IndexKind.Number, typescript_1.default.IndexKind.String]
: [typescript_1.default.IndexKind.String];
for (const kind of kinds) {
const indexInfo = checker.getIndexInfoOfType(objectType, kind);
if (indexInfo) {
return indexInfo.declaration
? [indexInfo.declaration]
: (objectType.aliasSymbol ?? objectType.getSymbol())?.declarations;
}
}
return undefined;
}