intellify
Version:
Detect JavaScript & TypeScript errors and make codes more optimized.
583 lines (455 loc) ⢠18.8 kB
JavaScript
console.clear();
import fs from 'fs';
import path from 'path';
import { Command } from 'commander';
import chalk from 'chalk';
import * as acorn from 'acorn';
import * as walk from 'acorn-walk';
import { globSync } from 'glob';
import readline from 'readline';
import boxen from 'boxen';
const program = new Command();
program
.name('intellify')
.description('Optimize your JavaScript projects by detecting errors and unused code')
.version('1.0.0')
.action(analyze);
try {
program.parse();
} catch (error) {
console.error(chalk.red.bold('\nš„ Top-level application error:'), error);
process.exit(1);
}
async function analyze() {
try {
console.clear();
const headerText = `${chalk.bold.cyan('š§ INTELLIFY')}\n${chalk.cyan('Modern Code Intelligence Tool')}`;
console.log(boxen(headerText, {padding: 1, margin: 1, borderStyle: 'double', borderColor: 'cyan', textAlignment: 'center'}));
const projectPath = process.cwd();
const projectName = path.basename(projectPath);
console.log(chalk.blue(`š Project: `) + chalk.blue.bold(`${projectName}`) + chalk.blue(` (${projectPath})`));
const packageJsonPath = path.join(projectPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const projectInfo = {
name: packageJson.name || projectName,
version: packageJson.version || 'N/A',
description: packageJson.description || 'No description',
dependencies: Object.keys(packageJson.dependencies || {}).length,
devDependencies: Object.keys(packageJson.devDependencies || {}).length
};
const projectInfoText =
`${chalk.white.bold(`${projectInfo.name}@${projectInfo.version}`)}\n` +
`${chalk.white(projectInfo.description.substring(0,45))}${projectInfo.description.length > 45 ? '...' : ''}\n` +
`${chalk.white(`Total Dependencies: ${projectInfo.dependencies + projectInfo.devDependencies}`)}`;
console.log(boxen(projectInfoText, {title: 'Project Info', titleAlignment: 'center', padding: 1, margin: {top:1, bottom:1}, borderStyle: 'round', borderColor: 'blue'}));
} catch (error) {
console.log(chalk.yellow(boxen('Could not parse package.json', {padding:1, borderColor:'yellow'})));
}
}
const results = {
syntaxErrors: [],
unusedPackages: [],
unusedVariables: [],
commentLines: [],
analyzedFiles: []
};
const stats = {
filesAnalyzed: 0,
linesOfCode: 0,
fileTypes: {},
totalSize: 0
};
console.log(chalk.yellow('š Scanning files...'));
const spinnerFrames = ['ā ', 'ā ', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ', 'ā '];
let spinnerInterval;
let spinnerFrame = 0;
spinnerInterval = setInterval(() => {
process.stdout.write(`\r${chalk.yellow(spinnerFrames[spinnerFrame])} Analyzing...`);
spinnerFrame = (spinnerFrame + 1) % spinnerFrames.length;
}, 80);
await analyzeProject(projectPath, results, stats);
clearInterval(spinnerInterval);
process.stdout.write('\r' + ' '.repeat(30) + '\r');
displayResults(results, stats);
await promptFix(results, projectPath);
} catch (error) {
console.error(chalk.red.bold('\nš„ Error during analysis:'), error);
if (typeof spinnerInterval !== 'undefined' && spinnerInterval) {
clearInterval(spinnerInterval);
process.stdout.write('\r' + ' '.repeat(30) + '\r');
}
process.exit(1);
}
}
async function analyzeProject(projectPath, results, stats) {
try {
results.comments = [];
const patterns = [
'**/*.js',
'**/*.jsx',
'**/*.ts',
'**/*.tsx',
'**/*.vue',
'**/*.svelte'
];
const ignorePatterns = [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/.git/**',
'**/coverage/**',
'**/.next/**',
'**/out/**',
'**/.cache/**',
'**/public/build/**'
];
const files = globSync(patterns, {
ignore: ignorePatterns,
cwd: projectPath,
absolute: true,
});
const packageJsonPath = path.join(projectPath, 'package.json');
let packageDependencies = [];
let packageDevDependencies = [];
if (fs.existsSync(packageJsonPath)) {
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(packageJsonContent);
packageDependencies = Object.keys(packageJson.dependencies || {});
packageDevDependencies = Object.keys(packageJson.devDependencies || {});
}
const importedPackages = new Set();
for (const file of files) {
try {
stats.filesAnalyzed++;
const ext = path.extname(file).slice(1).toLowerCase();
stats.fileTypes[ext] = (stats.fileTypes[ext] || 0) + 1;
const fileStat = fs.statSync(file);
stats.totalSize += fileStat.size;
const code = fs.readFileSync(file, 'utf-8');
const lineCount = code.split('\n').length;
stats.linesOfCode += lineCount;
const relativePath = path.relative(projectPath, file);
const fileInfo = {
name: path.basename(file),
path: relativePath,
size: fileStat.size,
lines: lineCount,
type: ext
};
results.analyzedFiles.push(fileInfo);
let ast;
try {
ast = acorn.parse(code, {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true,
allowHashBang: true,
allowAwaitOutsideFunction: true,
onComment: (isBlock, text, start, end, locStart, locEnd) => {
results.comments.push({ file, isBlock, text, start, end, locStart, locEnd });
}
});
} catch (parsingError) {
results.syntaxErrors.push({
file,
line: parsingError.loc ? parsingError.loc.line : 'unknown',
message: `Acorn parsing error: ${parsingError.message}`,
});
console.warn(chalk.yellow(`Skipping analysis of ${file} due to parsing error: ${parsingError.message}`));
continue;
}
if (ast) {
findUnusedVariables(file, ast, code, results);
findImportedPackages(ast, importedPackages);
}
} catch (fileProcessingError) {
console.error(chalk.red(`\nš„ Error processing file ${file} (outside parsing):`), fileProcessingError);
results.syntaxErrors.push({
file,
line: 'unknown',
message: `File processing error: ${fileProcessingError.message}`
});
}
}
findUnusedPackages(
packageDependencies,
packageDevDependencies,
importedPackages,
results
);
} catch (projectAnalysisError) {
console.error(chalk.red.bold('\nš„ Error during project analysis setup:'), projectAnalysisError);
throw projectAnalysisError;
}
}
function findComments(file, code, results) {
}
function findUnusedVariables(file, ast, code, results) {
const declarations = new Map();
const usages = new Set();
walk.simple(ast, {
VariableDeclarator(node) {
if (node.id.type === 'Identifier') {
declarations.set(node.id.name, {
node,
file,
line: getLineFromLoc(node.loc, code),
});
}
},
FunctionDeclaration(node) {
if (node.id && node.id.type === 'Identifier') {
declarations.set(node.id.name, {
node,
file,
line: getLineFromLoc(node.loc, code),
});
}
},
ClassDeclaration(node) {
if (node.id && node.id.type === 'Identifier') {
declarations.set(node.id.name, {
node,
file,
line: getLineFromLoc(node.loc, code),
});
}
},
Identifier(node) {
usages.add(node.name);
},
});
for (const [name, info] of declarations.entries()) {
if (!usages.has(name) || usages.has(name) && declarations.has(name)) {
const parent = info.node.parent;
if (parent && parent.type !== 'ExportNamedDeclaration' && parent.type !== 'ExportDefaultDeclaration') {
results.unusedVariables.push({
file: info.file,
name,
line: info.line,
});
}
}
}
}
function findImportedPackages(ast, importedPackages) {
walk.simple(ast, {
ImportDeclaration(node) {
const source = node.source.value;
if (!source.startsWith('.') && !source.startsWith('/')) {
const packageName = source.split('/')[0];
importedPackages.add(packageName);
}
},
CallExpression(node) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length &&
node.arguments[0].type === 'Literal'
) {
const source = node.arguments[0].value;
if (typeof source === 'string' && !source.startsWith('.') && !source.startsWith('/')) {
const packageName = source.split('/')[0];
importedPackages.add(packageName);
}
}
},
});
}
async function fixComments(allComments, projectPath) {
console.log(chalk.blue('\nš¬ Attempting to remove comments...'));
let commentsRemovedCount = 0;
let commentsSkippedCount = 0;
const commentsByFile = allComments.reduce((acc, comment) => {
if (!acc[comment.file]) {
acc[comment.file] = [];
}
acc[comment.file].push(comment);
return acc;
}, {});
for (const file in commentsByFile) {
let fileContent = fs.readFileSync(file, 'utf-8');
const originalFileContent = fileContent;
let fileModified = false;
const sortedComments = commentsByFile[file].sort((a, b) => b.start - a.start);
for (const comment of sortedComments) {
let tempFileContent = fileContent.substring(0, comment.start) + fileContent.substring(comment.end);
let lineOfChangeStart = comment.locStart.line;
let checkContent;
let tempLines = tempFileContent.split('\n');
let lineBeforeComment = originalFileContent.substring(0, comment.start).split('\n').pop();
let lineAfterComment = originalFileContent.substring(comment.end).split('\n').shift();
let isSafeToRemove = false;
if (lineBeforeComment.trim() === '' && lineAfterComment.trim() === '') {
isSafeToRemove = true;
} else {
checkContent = tempLines[lineOfChangeStart -1];
if (checkContent !== undefined && checkContent.trim() !== '') {
try {
acorn.parse(checkContent, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true, allowHashBang: true });
isSafeToRemove = true;
} catch (e) {
isSafeToRemove = false;
}
} else if (checkContent !== undefined && checkContent.trim() === '') {
isSafeToRemove = true;
}
}
if (isSafeToRemove) {
fileContent = tempFileContent;
commentsRemovedCount++;
fileModified = true;
} else {
commentsSkippedCount++;
console.log(chalk.yellow(` Skipped comment in ${path.basename(file)} at line ${comment.locStart.line} (potential syntax issue)`));
}
}
if (fileModified) {
try {
acorn.parse(fileContent, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true, allowAwaitOutsideFunction: true });
fs.writeFileSync(file, fileContent, 'utf-8');
console.log(chalk.green(` Updated ${path.basename(file)} with comment removals.`));
} catch (e) {
fs.writeFileSync(file, originalFileContent, 'utf-8');
console.log(chalk.red(` Failed to update ${path.basename(file)} due to syntax errors after attempting comment removals. File reverted.`));
commentsSkippedCount += sortedComments.length;
commentsRemovedCount -= sortedComments.length;
if(commentsRemovedCount < 0) commentsRemovedCount = 0;
}
}
}
if (commentsRemovedCount > 0) {
console.log(chalk.green.bold(`ā
Successfully removed ${commentsRemovedCount} comments.`));
}
if (commentsSkippedCount > 0) {
console.log(chalk.yellow.bold(`ā ļø Skipped ${commentsSkippedCount} comments due to potential syntax issues or batch failure.`));
}
if (commentsRemovedCount === 0 && commentsSkippedCount === 0 && allComments.length > 0) {
console.log(chalk.blue('No comments were removed or skipped (perhaps all were safe but no changes made, or no comments to remove).'));
}
}
function findUnusedPackages(dependencies, devDependencies, importedPackages, results) {
for (const dep of dependencies) {
if (!importedPackages.has(dep)) {
results.unusedPackages.push({
name: dep,
type: 'dependency',
});
}
}
for (const dep of devDependencies) {
if (!importedPackages.has(dep)) {
results.unusedPackages.push({
name: dep,
type: 'devDependency',
});
}
}
}
function getLineFromLoc(loc, code) {
if (!loc) return 'unknown';
return loc.start.line;
}
function displayResults(results, stats) {
console.log('\n⨠Analysis Complete āØ');
console.log();
}
async function promptFix(results, projectPath) {
if (results.syntaxErrors.length === 0 &&
results.unusedPackages.length === 0 &&
results.unusedVariables.length === 0 &&
(!results.comments || results.comments.length === 0)) {
console.log(chalk.green.bold('⨠Your project looks clean! No issues found.'));
return;
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let optimizationOptionsText = '';
if (results.unusedPackages.length > 0) {
optimizationOptionsText += `${chalk.yellow.bold('š¦ Remove unused packages')} ${chalk.yellow(`(${results.unusedPackages.length})`)}\n`;
}
if (results.unusedVariables.length > 0) {
optimizationOptionsText += `${chalk.magenta.bold('š Remove unused variables')} ${chalk.magenta(`(${results.unusedVariables.length})`)}\n`;
}
if (results.comments && results.comments.length > 0) {
optimizationOptionsText += `${chalk.blue.bold('š¬ Remove comments')} ${chalk.blue(`(${results.comments.length})`)}\n`;
}
if (optimizationOptionsText.length > 0) {
console.log(boxen(optimizationOptionsText.trim(), {title: 'Optimization Options', titleAlignment: 'center', padding: 1, margin: {top:1, bottom:1}, borderStyle: 'round', borderColor: 'cyan'}));
} else {
}
console.log(chalk.cyan.bold('Would you like to optimize your codebase?'));
const answer = 'yes';
console.log(chalk.cyan('š¬ (yes/no): yes'));
if (answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y') {
console.log(chalk.green.bold('\nš§ Starting optimization process...'));
if (results.unusedPackages.length > 0) {
console.log(chalk.yellow.bold('\nš¦ Would you like to remove unused packages?'));
const removePackages = 'yes';
console.log(chalk.yellow('š¬ (yes/no): yes'));
if (removePackages.toLowerCase() === 'yes' || removePackages.toLowerCase() === 'y') {
process.stdout.write(chalk.yellow('\n⬠Removing unused packages... '));
const progressChars = ['ā', 'ā', 'ā
', 'ā', 'ā', 'ā'];
let progressIndex = 0;
const progressInterval = setInterval(() => {
process.stdout.write(`\r${chalk.yellow('⬠Removing unused packages... ')}${chalk.yellow(progressChars[progressIndex])}`);
progressIndex = (progressIndex + 1) % progressChars.length;
}, 100);
await fixUnusedPackages(results.unusedPackages, projectPath);
clearInterval(progressInterval);
process.stdout.write(`\r${chalk.yellow('⬠Removing unused packages... ')}${chalk.green('ā Done!')}\n`);
}
}
if (results.unusedVariables.length > 0) {
console.log(chalk.magenta.bold('\nš Would you like to remove unused variables?'));
const removeVariables = 'yes';
console.log(chalk.magenta('š¬ (yes/no): yes'));
if (removeVariables.toLowerCase() === 'yes' || removeVariables.toLowerCase() === 'y') {
await fixUnusedVariables(results.unusedVariables, projectPath);
}
}
if (results.comments && results.comments.length > 0) {
console.log(chalk.blue.bold('\nš¬ Would you like to remove comments?'));
const removeComments = 'yes';
console.log(chalk.blue('š¬ (yes/no): yes'));
if (removeComments.toLowerCase() === 'yes' || removeComments.toLowerCase() === 'y') {
await fixComments(results.comments, projectPath);
}
}
console.log(chalk.green.bold('\nā
All selected optimizations have been applied!'));
console.log(chalk.green('š Your codebase is now faster and cleaner.'));
} else {
console.log(chalk.blue('\nš No problem! You can run intellify again anytime to optimize your code.'));
}
rl.close();
}
async function fixUnusedPackages(unusedPackages, projectPath) {
console.log(chalk.yellow('Removing unused packages from package.json...'));
const packageJsonPath = path.join(projectPath, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
let packagesRemoved = 0;
for (const pkg of unusedPackages) {
if (pkg.type === 'dependency' && packageJson.dependencies && packageJson.dependencies[pkg.name]) {
delete packageJson.dependencies[pkg.name];
packagesRemoved++;
} else if (pkg.type === 'devDependency' && packageJson.devDependencies && packageJson.devDependencies[pkg.name]) {
delete packageJson.devDependencies[pkg.name];
packagesRemoved++;
}
}
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log(chalk.green(`ā
Successfully removed ${packagesRemoved} unused packages from package.json!`));
}
async function fixUnusedVariables(unusedVariables, projectPath) {
console.log(chalk.magenta('This would remove unused variables from your code files.'));
console.log(chalk.yellow('Note: For safety reasons, this operation is not implemented in this version.'));
console.log(chalk.yellow('Please remove the following unused variables manually:'));
unusedVariables.forEach(variable => {
console.log(chalk.yellow(` ⢠${variable.name} in ${path.basename(variable.file)} (line ${variable.line})`));
});
}