UNPKG

knip

Version:

Find unused files, dependencies and exports in your TypeScript and JavaScript projects

184 lines (183 loc) 7.82 kB
import ts from 'typescript'; export function isGetOrSetAccessorDeclaration(node) { return node.kind === ts.SyntaxKind.SetAccessor || node.kind === ts.SyntaxKind.GetAccessor; } export 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 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 getTypeName = (node) => { if (!node.parent?.parent) return; const typeRef = findAncestor(node, _node => ts.isTypeReferenceNode(_node)); if (typeRef && ts.isQualifiedName(typeRef.typeName)) return typeRef.typeName; }; 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';