create new project
This commit is contained in:
parent
65d5ada710
commit
1e283fd3e0
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/__pycache__/
|
||||||
|
/castre.egg-info/
|
||||||
|
/build/
|
101
README.md
Normal file
101
README.md
Normal 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
136
castre.py
Normal 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
6
pyproject.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[project]
|
||||||
|
name = "castre"
|
||||||
|
version = "0.9"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
castre = "castre:castre"
|
Loading…
Reference in New Issue
Block a user