Skip to content

Commit

Permalink
feat: code audit for bloat and quality in language analyzer (#477)
Browse files Browse the repository at this point in the history
secureli-454

<!-- Include general description here -->
Audit of language analyzer for code bloat and quality

## Changes
<!-- A detailed list of changes -->
* Resulting changes from audit of language analyzer for code bloat and
quality.

## Testing
<!--
Mention updated tests and any manual testing performed.
Are aspects not yet tested or not easily testable?
Feel free to include screenshots if appropriate.
 -->
* All existing tests are passing.

## Clean Code Checklist
<!-- This is here to support you. Some/most checkboxes may not apply to
your change -->
- [x] Meets acceptance criteria for issue
- [ ] New logic is covered with automated tests
- [ ] Appropriate exception handling added
- [ ] Thoughtful logging included
- [ ] Documentation is updated
- [ ] Follow-up work is documented in TODOs
- [ ] TODOs have a ticket associated with them
- [x] No commented-out code included


<!--
Github-flavored markdown reference:
https://docs.github.com/en/get-started/writing-on-github
-->
  • Loading branch information
isaac-heist-slalom authored Mar 15, 2024
1 parent 83bedc1 commit a7a4611
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 55 deletions.
2 changes: 1 addition & 1 deletion secureli/actions/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def _install_secureli(
install_languages, always_yes
)
language_config_result = (
self.action_deps.language_support._build_pre_commit_config(
self.action_deps.language_support.build_pre_commit_config(
install_languages, lint_languages
)
)
Expand Down
83 changes: 35 additions & 48 deletions secureli/modules/language_analyzer/language_support.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,16 @@
from pathlib import Path
from typing import Callable, Iterable, Optional, Any
from typing import Callable, Iterable, Optional

import pydantic
import yaml
from secureli.modules.shared.models.config import HookConfiguration, LinterConfig, Repo
from secureli.modules.shared.models.language import LanguageMetadata
from secureli.modules.shared.models import language

import secureli.repositories.secureli_config as SecureliConfig
from secureli.modules.shared.abstractions.pre_commit import PreCommitAbstraction
from secureli.modules.language_analyzer import git_ignore, language_config
from secureli.modules.shared.utilities import hash_config


class BuildConfigResult(pydantic.BaseModel):
"""Result about building config for all laguages"""

successful: bool
languages_added: list[str]
config_data: dict
linter_configs: list[LinterConfig]
version: str


class LinterConfigWriteResult(pydantic.BaseModel):
"""
Result from writing linter config files
"""

successful_languages: list[str]
error_messages: list[str]


class LanguageSupportService:
"""
Orchestrates a growing list of security best practices for languages. Installs
Expand All @@ -52,9 +32,9 @@ def __init__(
def apply_support(
self,
languages: list[str],
language_config_result: BuildConfigResult,
language_config_result: language.BuildConfigResult,
overwrite_pre_commit: bool,
) -> LanguageMetadata:
) -> language.LanguageMetadata:
"""
Applies Secure Build support for the provided languages
:param languages: list of languages to provide support for
Expand Down Expand Up @@ -85,7 +65,7 @@ def apply_support(
# Add .secureli/ to the gitignore folder if needed
self.git_ignore.ignore_secureli_files()

return LanguageMetadata(
return language.LanguageMetadata(
version=language_config_result.version,
security_hook_id=self.secret_detection_hook_id(languages),
linter_config_write_errors=linter_config_write_result.error_messages,
Expand All @@ -100,7 +80,7 @@ def secret_detection_hook_id(self, languages: list[str]) -> Optional[str]:
:return: The hook ID to use for secrets analysis if supported, otherwise None.
"""
# lint_languages param can be an empty set since we only need secrets detection hooks
language_config = self._build_pre_commit_config(languages, [])
language_config = self.build_pre_commit_config(languages, [])
config = language_config.config_data
secrets_detecting_repos_data = self.data_loader(
"pre-commit/secrets_detecting_repos.yaml"
Expand Down Expand Up @@ -143,21 +123,14 @@ def get_configuration(self, languages: list[str]) -> HookConfiguration:
:param languages: list of languages to get config for.
:return: A serializable Configuration model
"""
config = self._build_pre_commit_config(languages, set(languages)).config_data
config = self.build_pre_commit_config(languages, set(languages)).config_data

def create_repo(raw_repo: dict) -> Repo:
return Repo(
repo=raw_repo.get("repo", "unknown"),
revision=raw_repo.get("rev", "unknown"),
hooks=[hook.get("id", "unknown") for hook in raw_repo.get("hooks", [])],
)

repos = [create_repo(raw_repo) for raw_repo in config.get("repos", [])]
repos = [self._create_repo(raw_repo) for raw_repo in config.get("repos", [])]
return HookConfiguration(repos=repos)

def _build_pre_commit_config(
def build_pre_commit_config(
self, languages: list[str], lint_languages: Iterable[str]
) -> BuildConfigResult:
) -> language.BuildConfigResult:
"""
Builds the final .pre-commit-config.yaml from all supported repo languages. Also returns any and all
linter configuration data.
Expand All @@ -171,15 +144,17 @@ def _build_pre_commit_config(
config_languages = [*languages, "base"]
config_lint_languages = [*lint_languages, "base"]

for language in config_languages:
include_linter = language in config_lint_languages
result = self.language_config.get_language_config(language, include_linter)
for config_language in config_languages:
include_linter = config_language in config_lint_languages
result = self.language_config.get_language_config(
config_language, include_linter
)
if result.config_data:
successful_languages.append(language)
successful_languages.append(config_language)
(
linter_configs.append(
LinterConfig(
language=language,
language=config_language,
linter_data=result.linter_config.linter_data,
)
)
Expand All @@ -192,18 +167,30 @@ def _build_pre_commit_config(
config = {"repos": config_data}
version = hash_config(yaml.dump(config))

return BuildConfigResult(
return language.BuildConfigResult(
successful=True if len(config_data) > 0 else False,
languages_added=successful_languages,
config_data=config,
version=version,
linter_configs=linter_configs,
)

def _create_repo(self, raw_repo: dict) -> Repo:
"""
Creates a repository containing pre-commit hooks from a raw dictionary object
:param raw_repo: dictionary containing repository data.
:return: repository containing pre-commit hooks
"""
return Repo(
repo=raw_repo.get("repo", "unknown"),
revision=raw_repo.get("rev", "unknown"),
hooks=[hook.get("id", "unknown") for hook in raw_repo.get("hooks", [])],
)

def _write_pre_commit_configs(
self,
all_linter_configs: list[LinterConfig],
) -> LinterConfigWriteResult:
) -> language.LinterConfigWriteResult:
"""
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
Expand All @@ -220,16 +207,16 @@ def _write_pre_commit_configs(
error_messages: list[str] = []
successful_languages: list[str] = []

for config, language in linter_config_data:
for config, config_language in linter_config_data:
try:
with open(Path(SecureliConfig.FOLDER_PATH / config.filename), "w") as f:
f.write(yaml.dump(config.settings))
successful_languages.append(language)
successful_languages.append(config_language)
except:
error_messages.append(
f"Failed to write {config.filename} linter config file for {language}"
f"Failed to write {config.filename} linter config file for {config_language}"
)

return LinterConfigWriteResult(
return language.LinterConfigWriteResult(
successful_languages=successful_languages, error_messages=error_messages
)
19 changes: 19 additions & 0 deletions secureli/modules/shared/models/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,22 @@ class LanguageMetadata(pydantic.BaseModel):
version: str
security_hook_id: Optional[str]
linter_config_write_errors: Optional[list[str]] = []


class BuildConfigResult(pydantic.BaseModel):
"""Result about building config for all laguages"""

successful: bool
languages_added: list[str]
config_data: dict
linter_configs: list[LinterConfig]
version: str


class LinterConfigWriteResult(pydantic.BaseModel):
"""
Result from writing linter config files
"""

successful_languages: list[str]
error_messages: list[str]
12 changes: 6 additions & 6 deletions tests/modules/language_analyzer/test_language_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def mock_loader_side_effect(resource):
languages = ["RadLang"]
lint_languages = [*languages]

build_config_result = language_support_service._build_pre_commit_config(
build_config_result = language_support_service.build_pre_commit_config(
languages, lint_languages
)

Expand Down Expand Up @@ -293,7 +293,7 @@ def test_that_language_support_throws_exception_when_language_config_file_cannot
languages = ["RadLang"]
lint_languages = [*languages]

build_config_result = language_support_service._build_pre_commit_config(
build_config_result = language_support_service.build_pre_commit_config(
languages, lint_languages
)

Expand Down Expand Up @@ -325,7 +325,7 @@ def test_that_language_support_handles_invalid_language_config(
languages = ["RadLang"]
lint_languages = [*languages]

build_config_result = language_support_service._build_pre_commit_config(
build_config_result = language_support_service.build_pre_commit_config(
languages, lint_languages
)

Expand Down Expand Up @@ -357,7 +357,7 @@ def test_that_language_support_handles_empty_repos_list(
languages = ["RadLang"]
lint_languages = [*languages]

build_config_result = language_support_service._build_pre_commit_config(
build_config_result = language_support_service.build_pre_commit_config(
languages, lint_languages
)

Expand Down Expand Up @@ -422,7 +422,7 @@ def test_write_pre_commit_configs_returns_error_messages(

mock_open.assert_called_once()
mock_open.return_value.write.assert_not_called()
assert result == language_support.LinterConfigWriteResult(
assert result == language.LinterConfigWriteResult(
error_messages=[
f"Failed to write {mock_filename} linter config file for {mock_language}"
],
Expand All @@ -438,7 +438,7 @@ def test_write_pre_commit_configs_handles_empty_lint_configs(

mock_open.assert_not_called()
mock_open.return_value.write.assert_not_called()
assert result == language_support.LinterConfigWriteResult(
assert result == language.LinterConfigWriteResult(
error_messages=[],
successful_languages=[],
)

0 comments on commit a7a4611

Please sign in to comment.