create new project

This commit is contained in:
falsycat 2023-08-27 10:10:50 +09:00
parent 65d5ada710
commit 1e283fd3e0
4 changed files with 246 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/__pycache__/
/castre.egg-info/
/build/

101
README.md Normal file
View File

@ -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 <iostream>
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 <iostream>
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};
}
```

136
castre.py Normal file
View File

@ -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)

6
pyproject.toml Normal file
View File

@ -0,0 +1,6 @@
[project]
name = "castre"
version = "0.9"
[project.scripts]
castre = "castre:castre"