knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
268 lines (267 loc) • 11.3 kB
JavaScript
import ts from 'typescript';
import { FIX_FLAGS } from '../constants.js';
import { SymbolType } from '../types/issues.js';
function isGetOrSetAccessorDeclaration(node) {
return node.kind === ts.SyntaxKind.SetAccessor || node.kind === ts.SyntaxKind.GetAccessor;
}
function isPrivateMember(node) {
return node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.PrivateKeyword) ?? false;
}
export function isDefaultImport(node) {
return node.kind === ts.SyntaxKind.ImportDeclaration && !!node.importClause && !!node.importClause.name;
}
export function isAccessExpression(node) {
return ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node);
}
export function isImportCall(node) {
return (node.kind === ts.SyntaxKind.CallExpression &&
node.expression.kind === ts.SyntaxKind.ImportKeyword);
}
export function isRequireCall(callExpression) {
if (callExpression.kind !== ts.SyntaxKind.CallExpression) {
return false;
}
const { expression, arguments: args } = callExpression;
if (expression.kind !== ts.SyntaxKind.Identifier || expression.escapedText !== 'require') {
return false;
}
return args.length === 1;
}
export function isPropertyAccessCall(node, identifier) {
return (ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.getText() === identifier);
}
export const getNodeType = (node) => {
if (!node)
return SymbolType.UNKNOWN;
if (ts.isFunctionDeclaration(node))
return SymbolType.FUNCTION;
if (ts.isClassDeclaration(node))
return SymbolType.CLASS;
if (ts.isInterfaceDeclaration(node))
return SymbolType.INTERFACE;
if (ts.isTypeAliasDeclaration(node))
return SymbolType.TYPE;
if (ts.isEnumDeclaration(node))
return SymbolType.ENUM;
if (ts.isVariableDeclaration(node))
return SymbolType.VARIABLE;
return SymbolType.UNKNOWN;
};
export const isNonPrivatePropertyOrMethodDeclaration = (member) => (ts.isPropertyDeclaration(member) || ts.isMethodDeclaration(member) || isGetOrSetAccessorDeclaration(member)) &&
!isPrivateMember(member);
export const getClassMember = (member, isFixTypes) => ({
node: member,
identifier: member.name.getText(),
pos: member.name.getStart() + (ts.isComputedPropertyName(member.name) ? 1 : 0),
type: SymbolType.MEMBER,
fix: isFixTypes ? [member.getStart(), member.getEnd(), FIX_FLAGS.NONE] : undefined,
});
export const getEnumMember = (member, isFixTypes) => ({
node: member,
identifier: stripQuotes(member.name.getText()),
pos: member.name.getStart(),
type: SymbolType.MEMBER,
fix: isFixTypes
? [member.getStart(), member.getEnd(), FIX_FLAGS.OBJECT_BINDING | FIX_FLAGS.WITH_NEWLINE]
: undefined,
});
export function stripQuotes(name) {
const length = name.length;
if (length >= 2 && name.charCodeAt(0) === name.charCodeAt(length - 1) && isQuoteOrBacktick(name.charCodeAt(0))) {
return name.substring(1, length - 1);
}
return name;
}
var CharacterCodes;
(function (CharacterCodes) {
CharacterCodes[CharacterCodes["backtick"] = 96] = "backtick";
CharacterCodes[CharacterCodes["doubleQuote"] = 34] = "doubleQuote";
CharacterCodes[CharacterCodes["singleQuote"] = 39] = "singleQuote";
})(CharacterCodes || (CharacterCodes = {}));
function isQuoteOrBacktick(charCode) {
return (charCode === CharacterCodes.singleQuote ||
charCode === CharacterCodes.doubleQuote ||
charCode === CharacterCodes.backtick);
}
export function findAncestor(node, callback) {
node = node?.parent;
while (node) {
const result = callback(node);
if (result === 'STOP') {
return undefined;
}
if (result) {
return node;
}
node = node.parent;
}
return undefined;
}
export function findDescendants(node, callback) {
const results = [];
if (!node)
return results;
function visit(node) {
const result = callback(node);
if (result === 'STOP') {
return;
}
if (result) {
results.push(node);
}
ts.forEachChild(node, visit);
}
visit(node);
return results;
}
export const isDeclarationFileExtension = (extension) => extension === '.d.ts' || extension === '.d.mts' || extension === '.d.cts';
export const getJSDocTags = (node) => {
const tags = new Set();
let tagNodes = ts.getJSDocTags(node);
if (ts.isExportSpecifier(node) || ts.isBindingElement(node)) {
tagNodes = [...tagNodes, ...ts.getJSDocTags(node.parent.parent)];
}
else if (ts.isEnumMember(node) || ts.isClassElement(node)) {
tagNodes = [...tagNodes, ...ts.getJSDocTags(node.parent)];
}
else if (ts.isCallExpression(node)) {
tagNodes = [...tagNodes, ...ts.getJSDocTags(node.parent)];
}
for (const tagNode of tagNodes) {
const match = tagNode.getText()?.match(/@\S+/);
if (match)
tags.add(match[0]);
}
return tags;
};
export const getLineAndCharacterOfPosition = (node, pos) => {
const { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(pos);
return { line: line + 1, col: character + 1, pos };
};
const getMemberStringLiterals = (typeChecker, node) => {
if (ts.isElementAccessExpression(node)) {
if (ts.isStringLiteral(node.argumentExpression))
return [node.argumentExpression.text];
const type = typeChecker.getTypeAtLocation(node.argumentExpression);
if (type.isUnion())
return type.types.map(type => type.value);
}
if (ts.isPropertyAccessExpression(node)) {
return [node.name.escapedText];
}
};
export const getAccessMembers = (typeChecker, node) => {
let members = [];
let current = node.parent;
while (current) {
const ms = getMemberStringLiterals(typeChecker, current);
if (!ms)
break;
const joinIds = (id) => (members.length === 0 ? id : members.map(ns => `${ns}.${id}`));
members = members.concat(ms.flatMap(joinIds));
current = current.parent;
}
return members;
};
export const isDestructuring = (node) => node.parent &&
ts.isVariableDeclaration(node.parent) &&
ts.isVariableDeclarationList(node.parent.parent) &&
ts.isObjectBindingPattern(node.parent.name);
export const getDestructuredIds = (name) => name.elements.map(element => element.name.getText());
export const isConsiderReferencedNS = (node) => ts.isPropertyAssignment(node.parent) ||
ts.isShorthandPropertyAssignment(node.parent) ||
(ts.isCallExpression(node.parent) && node.parent.arguments.includes(node)) ||
ts.isSpreadAssignment(node.parent) ||
ts.isArrayLiteralExpression(node.parent) ||
ts.isExportAssignment(node.parent) ||
(ts.isVariableDeclaration(node.parent) && node.parent.initializer === node) ||
ts.isTypeQueryNode(node.parent);
const objectEnumerationMethods = new Set(['keys', 'entries', 'values', 'getOwnPropertyNames']);
export const isObjectEnumerationCallExpressionArgument = (node) => ts.isCallExpression(node.parent) &&
node.parent.arguments.includes(node) &&
ts.isPropertyAccessExpression(node.parent.expression) &&
ts.isIdentifier(node.parent.expression.expression) &&
node.parent.expression.expression.escapedText === 'Object' &&
objectEnumerationMethods.has(String(node.parent.expression.name.escapedText));
export const isInForIteration = (node) => node.parent && (ts.isForInStatement(node.parent) || ts.isForOfStatement(node.parent));
export const isTopLevel = (node) => ts.isSourceFile(node.parent) || (node.parent && ts.isSourceFile(node.parent.parent));
export const getTypeRef = (node) => {
if (!node.parent?.parent)
return;
return findAncestor(node, _node => ts.isTypeReferenceNode(_node));
};
export const isImportSpecifier = (node) => ts.isImportSpecifier(node.parent) ||
ts.isImportEqualsDeclaration(node.parent) ||
ts.isImportClause(node.parent) ||
ts.isNamespaceImport(node.parent);
const isInExportedNode = (node) => {
if (getExportKeywordNode(node))
return true;
return node.parent ? isInExportedNode(node.parent) : false;
};
export const isReferencedInExport = (node) => {
if (ts.isTypeQueryNode(node.parent) && isInExportedNode(node.parent.parent))
return true;
if (ts.isTypeReferenceNode(node.parent) && isInExportedNode(node.parent.parent))
return true;
return false;
};
export const getExportKeywordNode = (node) => node.modifiers?.find(mod => mod.kind === ts.SyntaxKind.ExportKeyword);
export const getDefaultKeywordNode = (node) => node.modifiers?.find(mod => mod.kind === ts.SyntaxKind.DefaultKeyword);
export const hasRequireCall = (node) => {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'require')
return true;
return node.getChildren().some(child => hasRequireCall(child));
};
export const isModuleExportsAccess = (node) => ts.isIdentifier(node.expression) && node.expression.escapedText === 'module' && node.name.escapedText === 'exports';
export const getImportMap = (sourceFile) => {
const importMap = new Map();
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement)) {
const importClause = statement.importClause;
const importPath = stripQuotes(statement.moduleSpecifier.getText());
if (importClause?.name)
importMap.set(importClause.name.text, importPath);
if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
for (const element of importClause.namedBindings.elements)
importMap.set(element.name.text, importPath);
}
}
}
return importMap;
};
export const getDefaultImportName = (importMap, specifier) => {
for (const [importName, importSpecifier] of importMap) {
if (importSpecifier === specifier)
return importName;
}
};
export const getPropertyValues = (node, propertyName) => {
const values = new Set();
if (ts.isObjectLiteralExpression(node)) {
const props = node.properties.find(prop => ts.isPropertyAssignment(prop) && prop.name.getText() === propertyName);
if (props && ts.isPropertyAssignment(props)) {
const initializer = props.initializer;
if (ts.isStringLiteral(initializer)) {
values.add(initializer.text);
}
else if (ts.isArrayLiteralExpression(initializer)) {
for (const element of initializer.elements) {
if (ts.isStringLiteral(element))
values.add(element.text);
}
}
else if (ts.isObjectLiteralExpression(initializer)) {
for (const prop of initializer.properties) {
if (ts.isPropertyAssignment(prop)) {
if (ts.isStringLiteral(prop.initializer))
values.add(prop.initializer.text);
}
}
}
}
}
return values;
};