Skip to content

Commit

Permalink
Refactoring old code, Added more comments, fixing development depende…
Browse files Browse the repository at this point in the history
…ncies versions
  • Loading branch information
cacharle committed Sep 3, 2023
1 parent 87676af commit ce01933
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 94 deletions.
37 changes: 19 additions & 18 deletions c_formatter_42/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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())
64 changes: 36 additions & 28 deletions c_formatter_42/formatters/align.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,63 +10,69 @@
# #
# ############################################################################ #

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<prefix>\s*(typedef\s+)? # Maybe a typedef
(struct|enum|union)) # Followed by a struct, enum or union
\s*(?P<suffix>[a-zA-Z_]\w+)?$ # Name of the type declaration
""",
re.X,
)
TYPEDECL_CLOSE_REGEX = re.compile(
r"""^(?P<prefix>\})\s* # Closing } followed by any amount of spaces
(?P<suffix>([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
global: for function prototypes
"""

lines = content.split("\n")
aligned = []
# select regex according to scope
if scope is Scope.LOCAL:
if scope == "local":
align_regex = "^\t" r"(?P<prefix>{type})\s+" r"(?P<suffix>\**{decl};)$"
elif scope is Scope.GLOBAL:
elif scope == "global":
align_regex = (
r"^(?P<prefix>{type})\s+"
r"(?P<suffix>({name}\(.*\)?;?)|({decl}(;|(\s+=\s+.*))))$"
)
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<prefix>\s*(typedef\s+)?(struct|enum|union))"
r"\s*(?P<suffix>[a-zA-Z_]\w+)?$"
)
typedecl_close_regex = r"^(?P<prefix>\})\s*(?P<suffix>([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 != "};":
Expand All @@ -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
5 changes: 2 additions & 3 deletions c_formatter_42/formatters/clang_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions c_formatter_42/formatters/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*"
Expand All @@ -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"
Expand All @@ -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<body>.*?)\n\}\n".replace(r"\n", "\n"),
get_replacement,
replacement_func,
content,
flags=re.DOTALL,
)
Expand Down
21 changes: 11 additions & 10 deletions c_formatter_42/formatters/hoist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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<indent>\s+)"
Expand All @@ -54,27 +56,26 @@ def hoist(content: str) -> str:
r"(?P<value>.+);$".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)
14 changes: 6 additions & 8 deletions c_formatter_42/formatters/line_breaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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


Expand All @@ -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


Expand All @@ -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:
Expand All @@ -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")
8 changes: 3 additions & 5 deletions c_formatter_42/formatters/preprocessor_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@

def preprocessor_directive(content: str) -> str:
lines = content.split("\n")

directive_regex = r"^\#\s*(?P<name>[a-z]+)\s?(?P<rest>.*)$"
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
Expand All @@ -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)
File renamed without changes
Loading

0 comments on commit ce01933

Please sign in to comment.