Remove unused modules across project

Well gentlemen, I managed to pull this off. Here are the notes for my future self, or anyone else:

  • Clear out unused imports across the project using IntelliJ’s Elm plugin:


image

  • Use the following script to clear out unused modules, including the resulting empty folders.
import * as fs from 'fs-extra';
import * as path from 'path';

export {
  removeUnusedElmModules,
}

function removeUnusedElmModules(
  directory = 'src/scripts',
  entryModuleName = 'MainModule',
  excludeDirectories = [
    'src/scripts/Api',
    'src/scripts/ElmFramework',
    'review/src',
  ],
): void {
  const allElmPaths = findFiles(directory, excludeDirectories, '.elm');
  const allUsedElmPaths = getUsedElmPaths(directory, entryModuleName);
  const allUnusedElmPaths = excludeMembers(allElmPaths, allUsedElmPaths);
  removeFiles(allUnusedElmPaths);
  removeEmptyDirectories(directory);
}


function getUsedElmPaths(directoryPath: string, entryModuleName: string): string[] {
  return getUsedModules(directoryPath, entryModuleName)
    .map(moduleName => getModulePath(directoryPath, moduleName));

  function getUsedModules(baseDir: string, entryModuleName: string): string[] {
    const visitedModules = exploreModule(baseDir, new Set(), entryModuleName);
    return Array.from(visitedModules);
  }

  function exploreModule(baseDir: string, visitedModules: Set<string>, moduleName: string): Set<string> {
    if (visitedModules.has(moduleName)) return visitedModules;
    const modulePath = getModulePath(baseDir, moduleName);
    if (!fs.existsSync(modulePath)) return visitedModules;
    visitedModules.add(moduleName);
    const content = readModuleContent(modulePath);
    const imports = extractImports(content);
    imports.forEach((importName) => exploreModule(baseDir, visitedModules, importName));
    return visitedModules;
  }

  function readModuleContent(modulePath: string): string {
    return fs.readFileSync(modulePath, 'utf-8');
  }

  function extractImports(content: string): string[] {
    return (content.match(/^import\s+([^\s]+)/gm) || []).map((line) => line.split(' ')[1]);
  }

  function getModulePath(baseDir: string, moduleName: string): string {
    return path.join(baseDir, moduleName.replace(/\./g, '/') + '.elm');
  }
}

function removeFiles(filePaths: string[]): void {
  filePaths.forEach(filePath => {
    try {
      fs.unlinkSync(filePath);
    } catch (error) {
      console.error(`Failed to delete ${filePath}:`, error);
    }
  });
}

function excludeMembers(toChange: string[], excludeThese: string[]): string[] {
  const setB = new Set(excludeThese);
  return toChange.filter(item => !setB.has(item));
}


function findFiles(directory: string, excludedPaths: string[], extension: string): string[] {
  return excludedPaths.includes(directory)
    ? []
    : fs.readdirSync(directory).flatMap(decider);

  function decider(candidate: string): string[] {
    const fullPath = path.join(directory, candidate);
    const stat = fs.statSync(fullPath);

    return stat.isDirectory()
      ? findFiles(fullPath, excludedPaths, extension)
      : path.extname(candidate) === extension
        ? [fullPath]
        : [];
  }
}


function removeEmptyDirectories(directory: string): void {
  const files = fs.readdirSync(directory);

  files.forEach(file => {
    const fullPath = path.join(directory, file);
    const stat = fs.statSync(fullPath);

    if (stat.isDirectory()) {
      removeEmptyDirectories(fullPath);
    }
  });

  if (fs.readdirSync(directory).length === 0) {
    fs.rmdirSync(directory);
  }
}

Jeroen, I’ll try to reach out in a couple of days to see if you can debug the elm-review issues.

Cheers!

4 Likes