From 816cb6f6d8d87a2d34bc3097e0733c9c05b54890 Mon Sep 17 00:00:00 2001 From: Charles Cabergs Date: Sun, 3 Sep 2023 12:22:19 +0200 Subject: [PATCH] Refactoring old code, Added more comments, fixing development dependencies versions --- c_formatter_42/__main__.py | 37 +++++----- c_formatter_42/formatters/align.py | 64 ++++++++++-------- c_formatter_42/formatters/clang_format.py | 5 +- c_formatter_42/formatters/helper.py | 15 ++-- c_formatter_42/formatters/hoist.py | 21 +++--- c_formatter_42/formatters/line_breaker.py | 14 ++-- .../formatters/preprocessor_directive.py | 8 +-- {Img => img}/final_back.png | Bin requirements-dev.txt | 29 ++++---- tests/formatters/test_align.py | 10 +-- 10 files changed, 109 insertions(+), 94 deletions(-) rename {Img => img}/final_back.png (100%) diff --git a/c_formatter_42/__main__.py b/c_formatter_42/__main__.py index 4726e80..124a5f9 100644 --- a/c_formatter_42/__main__.py +++ b/c_formatter_42/__main__.py @@ -18,7 +18,7 @@ from c_formatter_42.run import run_all -def main(): +def main() -> int: arg_parser = argparse.ArgumentParser( prog="c_formatter_42", description="Format C source according to the norm", @@ -41,23 +41,24 @@ def main(): if len(args.filepaths) == 0: content = sys.stdin.read() print(run_all(content), end="") - else: - for filepath in args.filepaths: - try: - with open(filepath, "r") as file: - content = file.read() - if args.confirm: - result = input( - f"Are you sure you want to overwrite {filepath}?[y/N]" - ) - if result != "y": - continue - print(f"Writing to {filepath}") - with open(filepath, "w") as file: - file.write(run_all(content)) - except OSError as e: - print(f"Error: {e.filename}: {e.strerror}") + return 0 + + for filepath in args.filepaths: + try: + with open(filepath, "r") as file: + content = file.read() + if args.confirm: + result = input(f"Are you sure you want to overwrite {filepath}?[y/N]") + if result != "y": + continue + print(f"Writing to {filepath}") + with open(filepath, "w") as file: + file.write(run_all(content)) + except OSError as e: + print(f"Error: {e.filename}: {e.strerror}", file=sys.stderr) + return 1 + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/c_formatter_42/formatters/align.py b/c_formatter_42/formatters/align.py index 7ec54c3..f199a54 100644 --- a/c_formatter_42/formatters/align.py +++ b/c_formatter_42/formatters/align.py @@ -10,19 +10,32 @@ # # # ############################################################################ # +from __future__ import annotations import re -from enum import Enum +import typing -from c_formatter_42.formatters import helper +if typing.TYPE_CHECKING: + from typing import Literal +from c_formatter_42.formatters import helper -class Scope(Enum): - LOCAL = 0 - GLOBAL = 1 +TYPEDECL_OPEN_REGEX = re.compile( + r"""^(?P\s*(typedef\s+)? # Maybe a typedef + (struct|enum|union)) # Followed by a struct, enum or union + \s*(?P[a-zA-Z_]\w+)?$ # Name of the type declaration + """, + re.X, +) +TYPEDECL_CLOSE_REGEX = re.compile( + r"""^(?P\})\s* # Closing } followed by any amount of spaces + (?P([a-zA-Z_]\w+)?;)$ # Name of the type (if typedef used) + """, + re.X, +) -def align_scope(content: str, scope: Scope) -> str: +def align_scope(content: str, scope: Literal["local", "global"]) -> str: """Align content scope can be either local or global local: for variable declarations in function @@ -30,11 +43,10 @@ def align_scope(content: str, scope: Scope) -> str: """ lines = content.split("\n") - aligned = [] # select regex according to scope - if scope is Scope.LOCAL: + if scope == "local": align_regex = "^\t" r"(?P{type})\s+" r"(?P\**{decl};)$" - elif scope is Scope.GLOBAL: + elif scope == "global": align_regex = ( r"^(?P{type})\s+" r"(?P({name}\(.*\)?;?)|({decl}(;|(\s+=\s+.*))))$" @@ -42,31 +54,25 @@ def align_scope(content: str, scope: Scope) -> str: align_regex = align_regex.format( type=helper.REGEX_TYPE, name=helper.REGEX_NAME, decl=helper.REGEX_DECL_NAME ) - # get the lines to be aligned - matches = [re.match(align_regex, line) for line in lines] + lines_to_be_aligned = [re.match(align_regex, line) for line in lines] aligned = [ (i, match.group("prefix"), match.group("suffix")) - for i, match in enumerate(matches) + for i, match in enumerate(lines_to_be_aligned) if match is not None and match.group("prefix") not in ["struct", "union", "enum"] ] - # global type declaration (struct/union/enum) - if scope is Scope.GLOBAL: - typedecl_open_regex = ( - r"^(?P\s*(typedef\s+)?(struct|enum|union))" - r"\s*(?P[a-zA-Z_]\w+)?$" - ) - typedecl_close_regex = r"^(?P\})\s*(?P([a-zA-Z_]\w+)?;)$" + # Global type declaration (struct/union/enum) + if scope == "global": in_type_scope = False for i, line in enumerate(lines): - m = re.match(typedecl_open_regex, line) + m = TYPEDECL_OPEN_REGEX.match(line) if m is not None: in_type_scope = True if m.group("suffix") is not None and "typedef" not in m.group("prefix"): aligned.append((i, m.group("prefix"), m.group("suffix"))) continue - m = re.match(typedecl_close_regex, line) + m = TYPEDECL_CLOSE_REGEX.match(line) if m is not None: in_type_scope = False if line != "};": @@ -83,27 +89,29 @@ def align_scope(content: str, scope: Scope) -> str: if m is not None: aligned.append((i, m.group("prefix"), m.group("suffix"))) - # get the minimum alignment required for each line + # Minimum alignment required for each line min_alignment = max( - (len(prefix.replace("\t", " " * 4)) // 4 + 1 for _, prefix, _ in aligned), + (len(prefix.expandtabs(4)) // 4 + 1 for _, prefix, _ in aligned), default=1, ) for i, prefix, suffix in aligned: - alignment = len(prefix.replace("\t", " " * 4)) // 4 + alignment = len(prefix.expandtabs(4)) // 4 lines[i] = prefix + "\t" * (min_alignment - alignment) + suffix - if scope is Scope.LOCAL: - lines[i] = "\t" + lines[i] + if scope == "local": + lines[i] = ( + "\t" + lines[i] + ) # Adding one more indent for inside the type declaration return "\n".join(lines) @helper.locally_scoped def align_local(content: str) -> str: """Wrapper for align_scope to use local_scope decorator""" - return align_scope(content, scope=Scope.LOCAL) + return align_scope(content, scope="local") def align(content: str) -> str: """Align the content in global and local scopes""" - content = align_scope(content, scope=Scope.GLOBAL) + content = align_scope(content, scope="global") content = align_local(content) return content diff --git a/c_formatter_42/formatters/clang_format.py b/c_formatter_42/formatters/clang_format.py index b5ef3a3..1f8544f 100644 --- a/c_formatter_42/formatters/clang_format.py +++ b/c_formatter_42/formatters/clang_format.py @@ -10,19 +10,18 @@ # # # ############################################################################ # +import contextlib import subprocess import sys -from contextlib import contextmanager from pathlib import Path import c_formatter_42.data CONFIG_FILENAME = Path(".clang-format") - DATA_DIR = Path(c_formatter_42.data.__file__).parent -@contextmanager +@contextlib.contextmanager def _config_context(): """Temporarly place .clang-format config file in the current directory If there already is a config in the current directory, it's backed up diff --git a/c_formatter_42/formatters/helper.py b/c_formatter_42/formatters/helper.py index 7f8656f..107bf16 100644 --- a/c_formatter_42/formatters/helper.py +++ b/c_formatter_42/formatters/helper.py @@ -9,8 +9,13 @@ # Updated: 2023/07/17 02:28:52 by kiokuless ### ########.fr # # # # **************************************************************************** # +from __future__ import annotations import re +import typing + +if typing.TYPE_CHECKING: + from typing import Callable # regex for a type REGEX_TYPE = r"(?!return|goto)([a-z]+\s+)*[a-zA-Z_]\w*" @@ -20,13 +25,13 @@ REGEX_DECL_NAME = r"\(?{name}(\[.*\])*(\)\(.*\))?".format(name=REGEX_NAME) -def locally_scoped(func): +def locally_scoped(func: Callable[[str], str]) -> Callable[[str], str]: """Apply the formatter on every local scopes of the content""" def wrapper(content: str) -> str: - def get_replacement(match): - body = match.group("body").strip("\n") - result = func(body) + def replacement_func(match: re.Match) -> str: + result = func(match.group("body").strip("\n")) + # Edge case for functions with empty bodies (See PR#31) if result.strip() == "": return ")\n{\n}\n" return ")\n{\n" + result + "\n}\n" @@ -35,7 +40,7 @@ def get_replacement(match): # `*?` is the non greedy version of `*` # https://docs.python.org/3/howto/regex.html#greedy-versus-non-greedy r"\)\n\{(?P.*?)\n\}\n".replace(r"\n", "\n"), - get_replacement, + replacement_func, content, flags=re.DOTALL, ) diff --git a/c_formatter_42/formatters/hoist.py b/c_formatter_42/formatters/hoist.py index 76be7ba..35d0c4b 100644 --- a/c_formatter_42/formatters/hoist.py +++ b/c_formatter_42/formatters/hoist.py @@ -14,6 +14,10 @@ import c_formatter_42.formatters.helper as helper +DECLARATION_REGEX = re.compile( + r"^\s*{t}\s+{d};$".format(t=helper.REGEX_TYPE, d=helper.REGEX_DECL_NAME) +) + @helper.locally_scoped def hoist(content: str) -> str: @@ -41,11 +45,9 @@ def hoist(content: str) -> str: char b; } } """ - input_lines = content.split("\n") - lines = [] - # split assignment + # Split assignment for line in input_lines: m = re.match( r"^(?P\s+)" @@ -54,27 +56,26 @@ def hoist(content: str) -> str: r"(?P.+);$".format(t=helper.REGEX_TYPE, d=helper.REGEX_DECL_NAME), line, ) + # If line is a declaration + assignment on the same line, + # create 2 new lines, one for the declaration and one for the assignment + # NOTE: edge case for array declarations which can't be hoisted (See #56) if m is not None and re.match(r".*\[.*\].*", m.group("name")) is None: lines.append(f"\t{m.group('type')}\t{m.group('name')};") lines.append( "{}{} = {};".format( m.group("indent"), - m.group("name").replace("*", ""), + m.group("name").replace("*", ""), # replace '*' for pointers m.group("value"), ) ) else: lines.append(line) - # hoist declarations and filter empty lines - decl_regex = r"^\s*{t}\s+{d};$".format( - t=helper.REGEX_TYPE, d=helper.REGEX_DECL_NAME - ) - declarations = [line for line in lines if re.match(decl_regex, line) is not None] + # Split declarations from body and remove empty lines + declarations = [line for line in lines if DECLARATION_REGEX.match(line) is not None] body = [line for line in lines if line not in declarations and line != ""] lines = declarations if len(declarations) != 0: lines.append("") lines.extend(body) - return "\n".join(lines) diff --git a/c_formatter_42/formatters/line_breaker.py b/c_formatter_42/formatters/line_breaker.py index bb069d6..ab5564a 100644 --- a/c_formatter_42/formatters/line_breaker.py +++ b/c_formatter_42/formatters/line_breaker.py @@ -39,8 +39,10 @@ def insert_break(line: str, column_limit: int) -> str: return line -def get_paren_depth(s: str) -> int: +def parenthesis_depth(s: str) -> int: paren_depth = 0 + # sq == single quote + # dq == double quote is_surrounded_sq = False is_surrounded_dq = False for c in s: @@ -52,7 +54,6 @@ def get_paren_depth(s: str) -> int: paren_depth += 1 elif c == ")" and not is_surrounded_sq and not is_surrounded_dq: paren_depth -= 1 - return paren_depth @@ -76,7 +77,7 @@ def get_paren_depth(s: str) -> int: # > > * baz())) Next line should be indented with 2 tabs (paren depth is 2) # ----------------------------------------------------------------------------------- def additional_indent_level(s: str, nest_indent_level: int = 0) -> int: - paren_depth = get_paren_depth(s) + paren_depth = parenthesis_depth(s) return nest_indent_level + paren_depth if paren_depth > 0 else 1 @@ -95,17 +96,15 @@ def additional_nest_indent_level(line: str) -> int: c == "=" and not is_surrounded_sq and not is_surrounded_dq - and get_paren_depth(line[:index]) == 0 + and parenthesis_depth(line[:index]) == 0 ) if is_assignation: break - return 1 if is_assignation else 0 def line_length(line: str) -> int: - line = line.expandtabs(4) - return len(line) + return len(line.expandtabs(4)) def indent_level(line: str) -> int: @@ -118,5 +117,4 @@ def indent_level(line: str) -> int: if last_tab_index == -1: return 0 return line_length(line[: last_tab_index + 1]) // 4 - return line.count("\t") diff --git a/c_formatter_42/formatters/preprocessor_directive.py b/c_formatter_42/formatters/preprocessor_directive.py index ed86469..7eb3afc 100644 --- a/c_formatter_42/formatters/preprocessor_directive.py +++ b/c_formatter_42/formatters/preprocessor_directive.py @@ -15,12 +15,11 @@ def preprocessor_directive(content: str) -> str: lines = content.split("\n") - directive_regex = r"^\#\s*(?P[a-z]+)\s?(?P.*)$" - matches = [re.match(directive_regex, line) for line in lines] + directive_lines = [re.match(directive_regex, line) for line in lines] idented = [ (i, match.group("name"), match.group("rest")) - for i, match in enumerate(matches) + for i, match in enumerate(directive_lines) if match is not None ] indent = 0 @@ -34,12 +33,11 @@ def preprocessor_directive(content: str) -> str: if directive_name == "endif": indent -= 1 - # if newline doesn't follows preprocessor part, insert one + # If newline doesn't follows preprocessor part, insert one (See PR#44) try: lastline_index = idented[-1][0] if lines[lastline_index + 1] != "": lines.insert(lastline_index + 1, "") except IndexError: pass - return "\n".join(lines) diff --git a/Img/final_back.png b/img/final_back.png similarity index 100% rename from Img/final_back.png rename to img/final_back.png diff --git a/requirements-dev.txt b/requirements-dev.txt index 91c9c08..d203403 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,12 +1,17 @@ -pytest -pytest-cov -pytest-timeout -six -pytest-clarity -mypy -flake8 -flake8_comprehensions -pep8_naming -tox -black -isort +pytest==7.4.1 +pytest-clarity==1.0.1 +pytest-cov==4.1.0 +pytest-timeout==2.1.0 +pytest-cov==4.1.0 +pytest-timeout==2.1.0 +six==1.16.0 +pytest-clarity==1.0.1 +mypy>=1.4.1 +mypy-extensions==1.0.0 +flake8>=5.0.4 +flake8-comprehensions==3.13.0 +pyls-flake8==0.4.0 +tox==4.11.1 +black==23.7.0 +isort==5.12.0 +pre-commit==3.4.0 diff --git a/tests/formatters/test_align.py b/tests/formatters/test_align.py index 222b132..047e170 100644 --- a/tests/formatters/test_align.py +++ b/tests/formatters/test_align.py @@ -12,7 +12,7 @@ import pytest -from c_formatter_42.formatters.align import Scope, align, align_local, align_scope +from c_formatter_42.formatters.align import align, align_local, align_scope def test_align_global_basic(): @@ -25,28 +25,28 @@ def test_align_global_basic(): int foo(); char bar(); """, - scope=Scope.GLOBAL, + scope="global", ) assert output == align_scope( """\ int\t\t\t\t\t\tfoo(); char bar(); """, - scope=Scope.GLOBAL, + scope="global", ) assert output == align_scope( """\ int\t\t\t \t\t\tfoo(); char \t bar(); """, - scope=Scope.GLOBAL, + scope="global", ) assert output == align_scope( """\ int\t\t\t \t\t\tfoo(); char \t bar(); """, - scope=Scope.GLOBAL, + scope="global", )