knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
96 lines (95 loc) • 4.81 kB
JavaScript
import parse, {} from '../../vendor/bash-parser/index.js';
import { Plugins, pluginArgsMap } from '../plugins.js';
import { debugLogObject } from '../util/debug.js';
import { toBinary, toDeferResolve } from '../util/input.js';
import { extractBinary } from '../util/modules.js';
import { relative } from '../util/path.js';
import { truncate } from '../util/string.js';
import { resolve as fallbackResolve } from './fallback.js';
import PackageManagerResolvers from './package-manager/index.js';
import { resolve as resolverFromPlugins } from './plugins.js';
import { parseNodeArgs } from './util.js';
const spawningBinaries = ['cross-env', 'retry-cli'];
const isExpansion = (node) => 'expansion' in node;
const isAssignment = (node) => 'type' in node && node.type === 'AssignmentWord';
export const getDependenciesFromScript = (script, options) => {
if (!script)
return [];
const fromArgs = (args, opts) => {
return getDependenciesFromScript(args.filter(arg => arg !== '--').join(' '), {
...options,
...opts,
knownBinsOnly: false,
});
};
const getDependenciesFromNodes = (nodes) => nodes.flatMap(node => {
switch (node.type) {
case 'Command': {
const text = node.name?.text;
const binary = text ? extractBinary(text) : text;
const commandExpansions = node.prefix
?.filter(isExpansion)
.map(prefix => prefix.expansion)
.flatMap(expansion => expansion.filter(expansion => expansion.type === 'CommandExpansion') ?? []) ?? [];
if (commandExpansions.length > 0) {
return (commandExpansions.flatMap(expansion => getDependenciesFromNodes(expansion.commandAST.commands)) ?? []);
}
if (!binary || binary === '.' || binary === 'source' || binary === '[')
return [];
if (binary.startsWith('-') || binary.startsWith('"') || binary.startsWith('..'))
return [];
const args = node.suffix?.map(arg => arg.text) ?? [];
if (['!', 'test'].includes(binary))
return fromArgs(args);
const fromNodeOptions = node.prefix
?.filter(isAssignment)
.filter(node => node.text.startsWith('NODE_OPTIONS='))
.flatMap(node => node.text.split('=')[1])
.map(arg => parseNodeArgs(arg.split(' ')))
.filter(args => args.require)
.flatMap(arg => arg.require)
.map(id => toDeferResolve(id)) ?? [];
if (binary in PackageManagerResolvers) {
const resolver = PackageManagerResolvers[binary];
return resolver(binary, args, { ...options, fromArgs });
}
if (pluginArgsMap.has(binary)) {
return [...resolverFromPlugins(binary, args, { ...options, fromArgs }), ...fromNodeOptions];
}
if (spawningBinaries.includes(binary)) {
const command = script.replace(new RegExp(`.*${text ?? binary}(\\s--\\s)?`), '');
return [toBinary(binary), ...getDependenciesFromScript(command, options)];
}
if (binary in Plugins) {
return [...fallbackResolve(binary, args, { ...options, fromArgs }), ...fromNodeOptions];
}
if (options.knownBinsOnly && !text?.startsWith('.'))
return [];
return [...fallbackResolve(binary, args, { ...options, fromArgs }), ...fromNodeOptions];
}
case 'LogicalExpression':
return getDependenciesFromNodes([node.left, node.right]);
case 'If':
return getDependenciesFromNodes([node.clause, node.then, ...(node.else ? [node.else] : [])]);
case 'For':
return getDependenciesFromNodes(node.do.commands);
case 'CompoundList':
return getDependenciesFromNodes(node.commands);
case 'Pipeline':
return getDependenciesFromNodes(node.commands);
case 'Function':
return getDependenciesFromNodes(node.body.commands);
default:
return [];
}
});
try {
const parsed = parse(script);
return parsed?.commands ? getDependenciesFromNodes(parsed.commands) : [];
}
catch (error) {
const msg = `Warning: failed to parse and ignoring script in ${relative(options.containingFilePath)} (${truncate(script, 30)})`;
debugLogObject('*', msg, error);
return [];
}
};