diff --git a/tools/alias_collision/.gitignore b/tools/alias_collision/.gitignore new file mode 100644 index 000000000..68bc17f9f --- /dev/null +++ b/tools/alias_collision/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/tools/alias_collision/check_alias_collision.py b/tools/alias_collision/check_alias_collision.py new file mode 100644 index 000000000..0aeba8eb7 --- /dev/null +++ b/tools/alias_collision/check_alias_collision.py @@ -0,0 +1,83 @@ +"""Check for alias collisions within the codebase""" + +from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, FileType +from pathlib import Path +from typing import List +from dataclasses import dataclass +import itertools +import re + + +ERROR_MESSAGE_TEMPLATE = """Found alias collision +Alias %s defined in %s already exists as %s in %s. +Consider renaming your alias. +""" + + +def dir_path(path_string: str) -> Path: + if Path(path_string).is_dir(): + return Path(path_string) + else: + raise NotADirectoryError(path_string) + + +def parse_arguments(): + parser = ArgumentParser( + description=__doc__, + formatter_class=ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "folder", + type=dir_path, + help="Folder to check", + ) + return parser.parse_args() + + +@dataclass(frozen=True) +class Alias: + + alias: str + value: str + module: Path + + +def find_aliases_in_file(file: Path) -> List[Alias]: + matches = re.findall(r"^alias (.*)='(.*)'", file.read_text(), re.M) + return [Alias(match[0], match[1], file) for match in matches] + + +def find_all_aliases(path: Path) -> List: + files = list(path.rglob("*.zsh")) + aliases = [find_aliases_in_file(file) for file in files] + return list(itertools.chain(*aliases)) + + +def check_for_duplicates(aliases: List[Alias]) -> None: + elements = dict() + for alias in aliases: + if alias.alias in elements: + existing = elements[alias.alias] + raise ValueError( + ERROR_MESSAGE_TEMPLATE + % ( + f"{alias.alias}={alias.value}", + alias.module.name, + f"{existing.alias}={existing.value}", + existing.module.name, + ) + ) + else: + elements[alias.alias] = alias + + +def main(): + """main""" + args = parse_arguments() + aliases = find_all_aliases(args.folder) + check_for_duplicates(aliases) + print("Found no collisions") + + +if __name__ == "__main__": + main() diff --git a/tools/alias_collision/requirements.txt b/tools/alias_collision/requirements.txt new file mode 100644 index 000000000..230c9dde1 --- /dev/null +++ b/tools/alias_collision/requirements.txt @@ -0,0 +1 @@ +pyfakefs diff --git a/tools/alias_collision/test/test_check_alias_collision.py b/tools/alias_collision/test/test_check_alias_collision.py new file mode 100644 index 000000000..0183ed901 --- /dev/null +++ b/tools/alias_collision/test/test_check_alias_collision.py @@ -0,0 +1,93 @@ +from pathlib import Path + +from pyfakefs.fake_filesystem_unittest import TestCase +from check_alias_collision import ( + dir_path, + find_all_aliases, + find_aliases_in_file, + check_for_duplicates, + Alias, +) + + +THREE_ALIASES = """ +alias g='git' + +alias ga='git add' +alias gaa='git add --all' +""" + +CONDITIONAL_ALIAS = """ +is-at-least 2.8 "$git_version" \ + && alias gfa='git fetch --all --prune --jobs=10' \ + || alias gfa='git fetch --all --prune' +""" + + +class CheckAliasCollisionTest(TestCase): + def setUp(self) -> None: + self.setUpPyfakefs() + + def test_dir_path__is_dir__input_path(self) -> None: + self.fs.create_dir("test") + self.assertEqual(Path("test"), dir_path("test")) + + def test_dir_path__is_file__raise_not_a_directory_error(self) -> None: + self.fs.create_file("test") + with self.assertRaises(NotADirectoryError): + dir_path("test") + + def test_dir_path__does_not_exist__raise_not_a_directory_error(self) -> None: + with self.assertRaises(NotADirectoryError): + dir_path("test") + + def test_find_all_aliases__empty_folder_should_return_empty_list(self) -> None: + self.fs.create_dir("test") + result = find_all_aliases(Path("test")) + self.assertListEqual([], result) + + def test_find_aliases_in_file__empty_text_should_return_empty_list(self) -> None: + self.fs.create_file("empty.zsh") + result = find_aliases_in_file(Path("empty.zsh")) + self.assertListEqual([], result) + + def test_find_aliases_in_file__one_alias_should_find_one(self) -> None: + self.fs.create_file("one.zsh", contents="alias g='git'") + result = find_aliases_in_file(Path("one.zsh")) + self.assertListEqual([Alias("g", "git", Path("one.zsh"))], result) + + def test_find_aliases_in_file__three_aliases_should_find_three(self) -> None: + self.fs.create_file("three.zsh", contents=THREE_ALIASES) + result = find_aliases_in_file(Path("three.zsh")) + self.assertListEqual( + [ + Alias("g", "git", Path("three.zsh")), + Alias("ga", "git add", Path("three.zsh")), + Alias("gaa", "git add --all", Path("three.zsh")), + ], + result, + ) + + def test_find_aliases_in_file__one_conditional_alias_should_find_none(self) -> None: + self.fs.create_file("conditional.zsh", contents=CONDITIONAL_ALIAS) + result = find_aliases_in_file(Path("conditional.zsh")) + self.assertListEqual([], result) + + def test_check_for_duplicates__no_duplicates_should_not_raise(self) -> None: + check_for_duplicates( + [ + Alias("g", "git", Path("git.zsh")), + Alias("ga", "git add", Path("git.zsh")), + Alias("gaa", "git add --all", Path("git.zsh")), + ] + ) + self.assertTrue(True) + + def test_check_for_duplicates__duplicates_should_raise(self) -> None: + with self.assertRaises(ValueError): + check_for_duplicates( + [ + Alias("gc", "git commit", Path("git.zsh")), + Alias("gc", "git clone", Path("git.zsh")), + ] + )