From a59fa0ba2a31084afe957a3e7a4944185ab7bc0a Mon Sep 17 00:00:00 2001 From: kevin-orlando <58826693+kevin-orlando@users.noreply.github.com> Date: Fri, 5 Jan 2024 13:25:30 -0500 Subject: [PATCH] fix: linter config file creation (#349) closes #344 Fixes issue where linter config files (i.e. .eslintrc.yaml) were not being created and also had generic file names. Files will now be created using the filename and config data contents specified in the specific yaml resources. --- secureli/container.py | 1 + .../files/configs/javascript.config.yaml | 12 +-- .../files/configs/typescript.config.yaml | 10 +-- secureli/services/language_config.py | 1 - secureli/services/language_support.py | 61 +++++++------- tests/services/test_language_support.py | 82 +++++++++++++++++-- 6 files changed, 119 insertions(+), 48 deletions(-) diff --git a/secureli/container.py b/secureli/container.py index f856b626..285717c8 100644 --- a/secureli/container.py +++ b/secureli/container.py @@ -108,6 +108,7 @@ class Container(containers.DeclarativeContainer): git_ignore=git_ignore_service, language_config=language_config_service, data_loader=read_resource, + echo=echo, ) """Analyzes a given repo to try to identify the most common language""" diff --git a/secureli/resources/files/configs/javascript.config.yaml b/secureli/resources/files/configs/javascript.config.yaml index 7d61b675..7660772b 100644 --- a/secureli/resources/files/configs/javascript.config.yaml +++ b/secureli/resources/files/configs/javascript.config.yaml @@ -1,6 +1,6 @@ -eslintrc: - extends: ["google"] - env: - es6: true - plugins: - - prettier +filename: ".eslintrc.yaml" +settings: + extends: ["google"] + env: + es6: true + plugins: ["prettier"] diff --git a/secureli/resources/files/configs/typescript.config.yaml b/secureli/resources/files/configs/typescript.config.yaml index 4afe7c51..85978b15 100644 --- a/secureli/resources/files/configs/typescript.config.yaml +++ b/secureli/resources/files/configs/typescript.config.yaml @@ -1,5 +1,5 @@ -eslintrc: - extends: ['google', 'plugin:@typescript-eslint/recommended', 'prettier'] - parser: '@typescript-eslint/parser' - plugins: - - '@typescript-eslint' +filename: ".eslintrc.yaml" +settings: + extends: ["google", "plugin:@typescript-eslint/recommended", "prettier"] + parser: "@typescript-eslint/parser" + plugins: ["@typescript-eslint"] diff --git a/secureli/services/language_config.py b/secureli/services/language_config.py index 34c9c011..0c4b701f 100644 --- a/secureli/services/language_config.py +++ b/secureli/services/language_config.py @@ -7,7 +7,6 @@ from secureli.resources.slugify import slugify from secureli.utilities.hash import hash_config from secureli.utilities.patterns import combine_patterns -import secureli.repositories.secureli_config as SecureliConfig class LanguageNotSupportedError(Exception): diff --git a/secureli/services/language_support.py b/secureli/services/language_support.py index 8476041a..8c35465f 100644 --- a/secureli/services/language_support.py +++ b/secureli/services/language_support.py @@ -3,6 +3,7 @@ import pydantic import yaml +from secureli.abstractions.echo import EchoAbstraction import secureli.repositories.secureli_config as SecureliConfig from secureli.abstractions.pre_commit import PreCommitAbstraction @@ -68,9 +69,18 @@ class UnexpectedReposResult(pydantic.BaseModel): unexpected_repos: Optional[list[str]] = [] +class LinterConfigData(pydantic.BaseModel): + """ + Represents the structure of a linter config file + """ + + filename: str + settings: Any + + class LinterConfig(pydantic.BaseModel): language: str - linter_data: list[Any] + linter_data: list[LinterConfigData] class BuildConfigResult(pydantic.BaseModel): @@ -95,11 +105,13 @@ def __init__( language_config: LanguageConfigService, git_ignore: GitIgnoreService, data_loader: Callable[[str], str], + echo: EchoAbstraction, ): self.git_ignore = git_ignore self.pre_commit_hook = pre_commit_hook self.language_config = language_config self.data_loader = data_loader + self.echo = echo def apply_support( self, languages: list[str], language_config_result: BuildConfigResult @@ -232,41 +244,28 @@ def _build_pre_commit_config( linter_configs=linter_configs, ) - @staticmethod def _write_pre_commit_configs( + self, all_linter_configs: list[LinterConfig], - ) -> LanguageLinterWriteResult: + ) -> None: """ Install any config files for given language to support any pre-commit commands. i.e. Javascript ESLint requires a .eslintrc file to sufficiently use plugins and allow for further customization for repo's flavor of Javascript - :return: LanguageLinterWriteResult + :param all_linter_configs: the applicable linter configs to create config files for in the repo """ - num_configs_success = 0 - num_configs_non_success = 0 - non_success_messages = list[str]() - - # parse through languages for their linter config if any. - for language_linter_configs in all_linter_configs: - # parse though each config for the given language. - for config in language_linter_configs.linter_data: - try: - config_name = list(config.keys())[0] - # generate relative file name and path. - config_file_name = f"{slugify(language_linter_configs.language)}.{config_name}.yaml" - path_to_config_file = ( - SecureliConfig.FOLDER_PATH / ".secureli/{config_file_name}" - ) - with open(path_to_config_file, "w") as f: - f.write(yaml.dump(config[config_name])) - num_configs_success += 1 - except Exception as e: - num_configs_non_success += 1 - non_success_messages.append(f"Unable to install config: {e}") - - return LanguageLinterWriteResult( - num_successful=num_configs_success, - num_non_success=num_configs_non_success, - non_success_messages=non_success_messages, - ) + linter_config_data = [ + (linter_data, config.language) + for config in all_linter_configs + for linter_data in config.linter_data + ] + + for config, language in linter_config_data: + try: + with open(Path(SecureliConfig.FOLDER_PATH / config.filename), "w") as f: + f.write(yaml.dump(config.settings)) + except: + self.echo.warning( + f"Failed to write {config.filename} config file for {language}" + ) diff --git a/tests/services/test_language_support.py b/tests/services/test_language_support.py index 031da6ec..cf137e90 100644 --- a/tests/services/test_language_support.py +++ b/tests/services/test_language_support.py @@ -7,7 +7,11 @@ from secureli.abstractions.pre_commit import ( InstallResult, ) -from secureli.services.language_support import BuildConfigResult, LanguageSupportService +from secureli.services.language_support import ( + LanguageSupportService, + LinterConfig, + LinterConfigData, +) from secureli.services.language_config import ( LanguageConfigService, LanguagePreCommitResult, @@ -32,6 +36,8 @@ def mock_open_config(mocker: MockerFixture): """ ) mocker.patch("builtins.open", mock_open) + mock_open.return_value.write = MagicMock() + return mock_open @pytest.fixture() @@ -77,6 +83,12 @@ def mock_git_ignore() -> MagicMock: return mock_git_ignore +@pytest.fixture() +def mock_echo() -> MagicMock: + mock_echo = MagicMock() + return mock_echo + + @pytest.fixture() def mock_language_config_service() -> LanguageConfigService: mock_language_config_service = MagicMock() @@ -90,12 +102,14 @@ def language_support_service( mock_git_ignore: MagicMock, mock_language_config_service: MagicMock, mock_data_loader: MagicMock, + mock_echo: MagicMock, ) -> LanguageSupportService: return LanguageSupportService( pre_commit_hook=mock_pre_commit_hook, git_ignore=mock_git_ignore, language_config=mock_language_config_service, data_loader=mock_data_loader, + echo=mock_echo, ) @@ -233,7 +247,7 @@ def mock_loader_side_effect(resource): version="abc123", linter_config=LoadLinterConfigsResult( successful=True, - linter_data=[{"key": {"example"}}], + linter_data=[{"filename": "test.txt", "settings": {}}], ), config_data=""" repos: @@ -267,7 +281,7 @@ def test_that_language_support_throws_exception_when_language_config_file_cannot version="abc123", linter_config=LoadLinterConfigsResult( successful=True, - linter_data=[{"key": {"example"}}], + linter_data=[{"filename": "test.txt", "settings": {}}], ), config_data=""" repos: @@ -301,7 +315,7 @@ def test_that_language_support_handles_invalid_language_config( version="abc123", linter_config=LoadLinterConfigsResult( successful=True, - linter_data=[{"key": {"example"}}], + linter_data=[{"filename": "test.txt", "settings": {}}], ), config_data="", ) @@ -328,7 +342,7 @@ def test_that_language_support_handles_empty_repos_list( version="abc123", linter_config=LoadLinterConfigsResult( successful=True, - linter_data=[{"key": {"example"}}], + linter_data=[{"filename": "test.txt", "settings": {}}], ), config_data=""" repos: @@ -345,3 +359,61 @@ def test_that_language_support_handles_empty_repos_list( ) assert build_config_result.config_data["repos"] == [] + + +def test_write_pre_commit_configs_writes_successfully( + language_support_service: LanguageSupportService, + mock_open: MagicMock, + mock_echo: MagicMock, +): + configs = [ + LinterConfig( + language="RadLag", + linter_data=[LinterConfigData(filename="rad-lint.yml", settings={})], + ), + LinterConfig( + language="CoolLang", + linter_data=[LinterConfigData(filename="cool-lint.yml", settings={})], + ), + ] + language_support_service._write_pre_commit_configs(configs) + + assert mock_open.call_count == len(configs) + assert mock_open.return_value.write.call_count == len(configs) + mock_echo.warning.assert_not_called() + + +def test_write_pre_commit_configs_ignores_empty_linter_arr( + language_support_service: LanguageSupportService, + mock_open: MagicMock, + mock_echo: MagicMock, +): + language_support_service._write_pre_commit_configs([]) + + mock_open.assert_not_called() + mock_open.return_value.write.assert_not_called() + mock_echo.warning.assert_not_called() + + +def test_write_pre_commit_configs_handle_exceptions( + language_support_service: LanguageSupportService, + mock_open: MagicMock, + mock_echo: MagicMock, +): + mock_open.side_effect = Exception("error") + mock_language = "CoolLang" + mock_filename = "cool-lint-config.yml" + language_support_service._write_pre_commit_configs( + [ + LinterConfig( + language=mock_language, + linter_data=[LinterConfigData(filename=mock_filename, settings={})], + ), + ] + ) + + mock_open.assert_called_once() + mock_open.return_value.write.assert_not_called() + mock_echo.warning.assert_called_once_with( + f"Failed to write {mock_filename} config file for {mock_language}" + )