From 1e283fd3e071d5085d175429929662df3ed70fc5 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sun, 27 Aug 2023 10:10:50 +0900 Subject: [PATCH] create new project --- .gitignore | 3 ++ README.md | 101 ++++++++++++++++++++++++++++++++++++ castre.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 6 +++ 4 files changed, 246 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 castre.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb37f5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/__pycache__/ +/castre.egg-info/ +/build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..8763549 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +castre +==== + +This is a python library for C++ AST-based REfactoring + +- easy +- simple +- minimal +- changes only where to refactor + +## How to Use + +1. install this package +1. write a script to refactor like an example below +1. execute the script +1. execute your formatter +1. verify and approve the changes by `git add -p` +1. save the approved changes by `git commit -m ...` +1. discard the unapproved changes by `git restore .` + +## Example + +refactoring script: +```python +import castre +import re + +def walker(item): + # The actual AST json data is stored at `item.raw` + # You can see it by `clang++ -Xclang -ast-dump=json -c [filename and options...]` + # And you can also see an AST structure in human-readable form by -ast-dump option without any value + # Please note that outputs of both commands are huge + if item.raw["kind"] == "DeclStmt" and item.raw["inner"][0]["kind"] == "VarDecl": + item.refactor(fix) + else: + for child in item: + walker(child) + +def fix(text): + if text.startswith("auto"): + text = "\n// meta comment to ignore violation in the next line\n" + text + else: + text = re.sub(r"^([^=]*)=(.*)$", r"\1{\2}", text, flags=re.S) + return text + +# parse cpp codes and queue refactoring tasks +fixer = castre.walk( + ["a.cc", "-I.", "-std=c++20"], + walker, + path_filter=lambda x: x is not None and x.startswith("/Users/falsycat")) + +# you can reuse the fixer +castre.walk( + ["b.cc", "-I.", "-std=c++20"], + walker, + fixer=fixer, + path_filter=lambda x: x is not None and x.startswith("/Users/falsycat")) + +# execute the tasks and apply changes to the actual files +fixer.fix() +``` + +before: +```cpp +#include + +int main() { + struct A { void f() { int a = 123; auto b = 1+2+3+4; } }; + int x = 123; +std::cout << "helloworld" +<< std::endl; + auto y = 123; + std::cout << "goodbye" << std::endl; + int + z + = + 123; +} +``` + +after: +```cpp +#include + +int main() { + struct A { void f() { int a { 123}; +// meta comment to ignore violation in the next line +auto b = 1+2+3+4; } }; + int x { 123}; +std::cout << "helloworld" +<< std::endl; + +// meta comment to ignore violation in the next line +auto y = 123; + std::cout << "goodbye" << std::endl; + int + z + { + 123}; +} +``` diff --git a/castre.py b/castre.py new file mode 100644 index 0000000..d3b7726 --- /dev/null +++ b/castre.py @@ -0,0 +1,136 @@ +import bisect +import json +import os +import subprocess + +def walk(args, walker, path_filter=None, fixer=None): + proc = subprocess.run(["clang++", "-Xclang", "-ast-dump=json", "-c", *args], capture_output=True) + if proc.returncode != 0: + print(proc.stderr.decode("utf-8")) + raise Exception("analysis failure") + + tree = json.loads(proc.stdout) + root = Item(tree) + + fixer = fixer if fixer is not None else Fixer() + file = None + path = None + for item in tree["inner"]: + if "file" in item["loc"]: + path = os.path.abspath(item["loc"]["file"]) + file = None + + if path_filter is not None and not path_filter(path): + continue + + if file is None: + file = fixer.makeFile(path) + + walker(Item(item, parent=root, file=file)) + + return fixer + +class Item: + def __init__(self, j, parent=None, file=None): + self.parent = parent + self.raw = j + self.file = None + + if parent is not None: + self.file = parent.file + if file is not None: + self.file = file + + def __iter__(self): + return ItemItr(self) + + def refactorable(self): + return self.file is not None and "file" not in self.raw["range"]["end"] + + def range(self): + if not self.refactorable(): + raise Exception("item is not refactorable") + + ra = self.raw["range"] + begin = ra["begin"]["offset"] + end = ra["end"]["offset"] + return (begin, end) + + def pos(self): + begin, end = self.range() + return (begin, end-begin) + + def refactor(self, text): + self.file.replace(*self.pos(), text) + + def insertBefore(self, text): + self.file.replace(range()[0], 0, text) + + def insertAfter(self, text): + self.file.replace(range()[1], 0, text) + +class ItemItr: + def __init__(self, item): + self.index = 0 + self.item = item + self.inner = self.item.raw["inner"] if "inner" in self.item.raw else None + + def __next__(self): + if self.inner == None or len(self.inner) <= self.index: + raise StopIteration() + ret = Item(self.inner[self.index], parent=self.item) + self.index += 1 + return ret + +class Fixer: + def __init__(self): + self.files = {} + + def makeFile(self, path): + if path in self.files: + return self.files[path] + ret = File(path); + self.files[path] = ret + return ret + + def fix(self): + for f in self.files.values(): + f.fix() + +class File: + def __init__(self, path): + self.path = path + self.tasks = [] + + def replace(self, offset, n, text): + idx = bisect.bisect(self.tasks, x=offset, key=lambda x: x[0]) + + if idx > 0: + prev = self.tasks[idx-1] + if prev[0]+prev[1] > offset: + raise Exception("change conflict") + + if idx < len(self.tasks): + next = self.tasks[idx] + if next[0] < offset+n: + raise Exception ("change conflict") + + self.tasks.insert(idx, (offset, n, text)) + + def dryFix(self): + with open(self.path, "r") as f: + src = f.read() + + for task in reversed(self.tasks): + begin = task[0] + end = begin + task[1] + text = task[2] + if not isinstance(text, str): + text = text(src[begin:end]) + src = src[0:begin] + text + src[end:] + return src + + def fix(self): + src = self.dryFix() + with open(self.path, "w") as f: + f.write(src) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..18553e0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "castre" +version = "0.9" + +[project.scripts] +castre = "castre:castre"