
The main changes are: * Split RuleCreator: move some of its content to RspecRepo and to RuleEditor in new modules. * Refactor tests accordingly. Other less important changes: * Sort and remove unnecessary imports * Remove unimplemented functions and unnecessary classes * Make some functions private * Move pushd from utils to tests where it is only used * Reduce code duplication here and there * Remove unnecessary Mock in some tests * Improve coverage for add_language_to_rule
150 lines
6.8 KiB
Python
150 lines
6.8 KiB
Python
import json
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Callable, Final, Iterable, Optional
|
|
|
|
import click
|
|
from github.PullRequest import PullRequest
|
|
|
|
from rspec_tools.errors import InvalidArgumentError
|
|
from rspec_tools.repo import RspecRepo, tmp_rspec_repo
|
|
from rspec_tools.utils import (LANG_TO_SOURCE, copy_directory_content,
|
|
get_label_for_language,
|
|
get_labels_for_languages, is_empty_metadata,
|
|
parse_and_validate_language_list, resolve_rule,
|
|
swap_metadata_files)
|
|
|
|
|
|
@contextmanager
|
|
def _rule_creator(token: str, user: Optional[str]):
|
|
with tmp_rspec_repo(token, user) as repo:
|
|
yield RuleCreator(repo)
|
|
|
|
|
|
def create_new_rule(languages: str, token: str, user: Optional[str]):
|
|
lang_list = parse_and_validate_language_list(languages)
|
|
label_list = get_labels_for_languages(lang_list)
|
|
with _rule_creator(token, user) as rule_creator:
|
|
rule_number = rule_creator.rspec_repo.reserve_rule_number()
|
|
rule_creator.create_new_rule_pull_request(token, rule_number, lang_list, label_list, user)
|
|
|
|
|
|
def add_language_to_rule(language: str, rule: str, token: str, user: Optional[str]):
|
|
label = get_label_for_language(language)
|
|
rule_number = resolve_rule(rule)
|
|
with _rule_creator(token, user) as rule_creator:
|
|
rule_creator.add_language_pull_request(token, rule_number, language, label, user)
|
|
|
|
|
|
class RuleCreator:
|
|
'''Create a new Rule in a repository following the official Github 'rspec' repository structure.'''
|
|
TEMPLATE_PATH: Final[Path] = Path(__file__).parent.parent.joinpath('rspec_template')
|
|
|
|
def __init__(self, rspec_repo: RspecRepo):
|
|
self.rspec_repo = rspec_repo
|
|
self.repo_dir = Path(self.rspec_repo.repository.working_dir)
|
|
|
|
def add_language_branch(self, rule_number: int, language: str) -> str:
|
|
'''Create and move files to add a new language to an existing rule.'''
|
|
branch_name = f'rule/S{rule_number}-add-{language}'
|
|
with self.rspec_repo.checkout_branch(self.rspec_repo.MASTER_BRANCH, branch_name):
|
|
rule_dir = self.repo_dir / 'rules' / f'S{rule_number}'
|
|
if not rule_dir.is_dir():
|
|
raise InvalidArgumentError(f"Rule \"S{rule_number}\" does not exist.")
|
|
lang_dir = rule_dir / language
|
|
if lang_dir.is_dir():
|
|
lang_url = f"https://github.com/SonarSource/rspec/tree/master/rules/S{rule_number}/{language}"
|
|
raise InvalidArgumentError(f"Rule \"S{rule_number}\" is already defined for language {language}. Modify the definition here: {lang_url}")
|
|
lang_dirs = [d for d in rule_dir.glob('*/') if d.is_dir()]
|
|
if 1 == len(list(lang_dirs)) and is_empty_metadata(rule_dir):
|
|
swap_metadata_files(rule_dir, lang_dirs[0])
|
|
lang_dir.mkdir()
|
|
|
|
lang_specific_template = self.TEMPLATE_PATH / 'multi_language' / 'language_specific'
|
|
copy_directory_content(lang_specific_template, lang_dir)
|
|
self._fill_in_the_blanks_in_the_template(lang_dir, rule_number)
|
|
self._fill_language_name_in_the_template(lang_dir, language)
|
|
self.rspec_repo.commit_all_and_push(f'Add {language} to rule S{rule_number}')
|
|
return branch_name
|
|
|
|
def create_new_rule_branch(self, rule_number: int, languages: Iterable[str]) -> str:
|
|
'''Create all the files required for a new rule.'''
|
|
branch_name = f'rule/add-RSPEC-S{rule_number}'
|
|
with self.rspec_repo.checkout_branch(self.rspec_repo.MASTER_BRANCH, branch_name):
|
|
rule_dir = self.repo_dir / 'rules' / f'S{rule_number}'
|
|
rule_dir.mkdir()
|
|
lang_count = sum(1 for _ in languages)
|
|
if lang_count > 1:
|
|
self._fill_multi_lang_template_files(rule_dir, rule_number, languages)
|
|
else:
|
|
self._fill_single_lang_template_files(rule_dir, rule_number, next(iter(languages)))
|
|
self.rspec_repo.commit_all_and_push(f'Create rule S{rule_number}')
|
|
|
|
return branch_name
|
|
|
|
def _fill_in_the_blanks_in_the_template(self, rule_dir: Path, rule_number: int):
|
|
for rule_item in rule_dir.glob('**/*'):
|
|
if rule_item.is_file():
|
|
template_content = rule_item.read_text()
|
|
final_content = template_content.replace('${RSPEC_ID}', str(rule_number))
|
|
rule_item.write_text(final_content)
|
|
|
|
def _fill_language_name_in_the_template(self, lang_dir: Path, language: str):
|
|
for rule_item in lang_dir.glob('*.adoc'):
|
|
if rule_item.is_file():
|
|
template_content = rule_item.read_text()
|
|
lang = LANG_TO_SOURCE[language]
|
|
final_content = template_content.replace('[source,text]', f'[source,{lang}]')
|
|
rule_item.write_text(final_content)
|
|
|
|
def _fill_multi_lang_template_files(self, rule_dir: Path, rule_number: int, languages: Iterable[str]):
|
|
common_template = self.TEMPLATE_PATH / 'multi_language' / 'common'
|
|
lang_specific_template = self.TEMPLATE_PATH / 'multi_language' / 'language_specific'
|
|
copy_directory_content(common_template, rule_dir)
|
|
|
|
for lang in languages:
|
|
lang_dir = rule_dir / lang
|
|
lang_dir.mkdir()
|
|
copy_directory_content(lang_specific_template, lang_dir)
|
|
self._fill_language_name_in_the_template(lang_dir, lang)
|
|
|
|
self._fill_in_the_blanks_in_the_template(rule_dir, rule_number)
|
|
|
|
def _fill_single_lang_template_files(self, rule_dir: Path, rule_number: int, language: str):
|
|
common_template = self.TEMPLATE_PATH / 'single_language' / 'common'
|
|
lang_specific_template = self.TEMPLATE_PATH / 'single_language' / 'language_specific'
|
|
copy_directory_content(common_template, rule_dir)
|
|
|
|
lang_dir = rule_dir /language
|
|
lang_dir.mkdir()
|
|
copy_directory_content(lang_specific_template, lang_dir)
|
|
|
|
self._fill_in_the_blanks_in_the_template(rule_dir, rule_number)
|
|
self._fill_language_name_in_the_template(lang_dir, language)
|
|
|
|
def add_language_pull_request(self, token: str, rule_number: int, language: str, label: str, user: Optional[str]):
|
|
branch_name = self.add_language_branch(rule_number, language)
|
|
click.echo(f'Created rule branch {branch_name}')
|
|
return self.rspec_repo.create_pull_request(
|
|
token,
|
|
branch_name,
|
|
f'Create rule S{rule_number}',
|
|
f'You can preview this rule [here](https://sonarsource.github.io/rspec/#/rspec/S{rule_number}/{language}) (updated a few minutes after each push).',
|
|
[label],
|
|
user
|
|
)
|
|
|
|
def create_new_rule_pull_request(self, token: str, rule_number: int, languages: Iterable[str], labels: Iterable[str], user: Optional[str]) -> PullRequest:
|
|
branch_name = self.create_new_rule_branch(rule_number, languages)
|
|
click.echo(f'Created rule branch {branch_name}')
|
|
first_lang = next(iter(languages))
|
|
return self.rspec_repo.create_pull_request(
|
|
token,
|
|
branch_name,
|
|
f'Create rule S{rule_number}',
|
|
f'You can preview this rule [here](https://sonarsource.github.io/rspec/#/rspec/S{rule_number}/{first_lang}) (updated a few minutes after each push).',
|
|
labels,
|
|
user
|
|
)
|
|
|