Last active
April 6, 2021 12:22
-
-
Save sylann/d698993edd63d4b24f6b14c2685f568d to your computer and use it in GitHub Desktop.
Find all missing imports of components in a javascript es6 project
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Find missing imports | |
Suitable for javascript projects using es6 imports. | |
Example uses: | |
Assuming SCRIPT is <path/to/find_missing_es6_imports.py> | |
First argument is a glob pattern to find components | |
Following arguments are paths to files or folders relative to current directory | |
- $ python3.9 SCRIPT '**/*.vue' components | |
- $ python3.9 SCRIPT '**/*.vue' components layouts pages | |
Note: | |
The script requires python3.9 | |
For python >=3.7,<3.9, add this import: `from __future__ import annotations` | |
For python >3,<3.7, remove typing annotations and replace f-strings with "".format() | |
""" | |
import pathlib | |
import re | |
import subprocess | |
from typing import Generator, Iterable, Iterator | |
def iter_component_uses(globs: Iterable[str]) -> Generator[tuple[str, str], None, None]: | |
"""Find occurences of used components throughout all files nested | |
in the given globs. | |
For each occurence, yield a tuple of (file_path, component_name) | |
where file_path is the relative path to a file using a component | |
and component_name is the name of the component used in this file | |
""" | |
p = subprocess.run( | |
("grep", "-roE", "<[A-Z]\w+", *globs), | |
stdout=subprocess.PIPE, | |
stderr=subprocess.PIPE, | |
) | |
p.check_returncode() | |
for line in p.stdout.decode("utf-8").split("\n"): | |
if not line.strip(): # skip empty lines | |
continue | |
if ":<" not in line: # Report malformed lines | |
print("Unexpected line:", line) | |
continue | |
yield line.split(":<") | |
class ComponentFile: | |
def __init__(self, path: str): | |
self.path = pathlib.Path(path) | |
self.content = self.path.read_text() | |
self.basename = self.path.name.replace(self.path.suffix, "") | |
# Storage of used components to check for missing imports | |
self.components_to_check: set[str] = set() | |
def find_component_use_occurences(iterator: Iterator[tuple[str, str]]) -> dict[str, ComponentFile]: | |
"""Store occurences of source components grouped by the name of the source file | |
that uses them. | |
""" | |
grouping = {} | |
for file_path, component_name in iterator: | |
if file_path not in grouping: | |
grouping[file_path] = ComponentFile(file_path) | |
grouping[file_path].components_to_check.add(component_name) | |
return grouping | |
def main(component_glob: str, sources: list[str]): | |
files = find_component_use_occurences(iterator=iter_component_uses(sources)) | |
# Iterate over source files nested in the components folder to check if they | |
# are used but not imported. | |
for component_path in pathlib.Path("components").glob(component_glob): | |
component_name = component_path.with_suffix("").name | |
for vf in files.values(): | |
if component_name not in vf.components_to_check: | |
continue | |
vf.components_to_check.remove(component_name) | |
is_not_imported = re.search( | |
fr"^\s*import {component_name}", | |
vf.content, | |
re.MULTILINE, | |
) is None | |
if is_not_imported: | |
print(f"{vf.path}: Missing import for {component_name}") | |
if __name__ == "__main__": | |
import argparse | |
p = argparse.ArgumentParser() | |
p.add_argument("component_glob", type=str) | |
p.add_argument("sources_globs", type=pathlib.Path, nargs="*") | |
args = p.parse_args() | |
main(args.component_glob, args.sources_globs) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note: Recursive components will be listed too. And possibly other problems that I did not encounter yet.
But this changes the problem from "finding something that might not exist" (PITA) to "simply check what is listed".