:Release Notes: - fixed get_usages bug. query awaited python, why value was installed to Python - added new method in minimal single file example, to search every value installed to some field - in TUI removed redundant input handler :Detailed Notes: - :Testing Performed: - manual testing :QA Notes: - :Issues Addressed: -
217 lines
6.8 KiB
Python
217 lines
6.8 KiB
Python
import tempfile
|
|
from typing import Any
|
|
|
|
import tree_sitter_cpp as tscpp
|
|
import tree_sitter_python as tspython
|
|
from arg_conf import parser as arg_parser
|
|
from command_runner import ICommandRunner
|
|
from log_conf import logger
|
|
from tree_sitter import Language, Parser, Query, QueryCursor, Tree
|
|
|
|
PY_LANGUAGE = Language(tspython.language())
|
|
CPP_LANGUAGE = Language(tscpp.language())
|
|
|
|
|
|
def arguments():
|
|
global arg_parser
|
|
subparsers = arg_parser.add_subparsers(
|
|
dest="command",
|
|
required=True,
|
|
)
|
|
|
|
module_parser = subparsers.add_parser("minimal")
|
|
|
|
module_parser.add_argument(
|
|
"--language", help="Setup parsing language", default="Python"
|
|
)
|
|
module_parser.add_argument("--file", "-f", help="File to parse", required=True)
|
|
logger.debug("Added Tree Sitter Minimal Example Arguments to the list")
|
|
|
|
|
|
class SingleFileParser(ICommandRunner):
|
|
def __init__(self, args):
|
|
self.normalized_language: Language
|
|
self.tree: Tree
|
|
self.parser: Parser
|
|
language = args.language.casefold()
|
|
if language in {"c++", "cpp"}:
|
|
self.parser = Parser(CPP_LANGUAGE)
|
|
self.normalized_language = CPP_LANGUAGE
|
|
self.language_name = "cpp"
|
|
else:
|
|
self.parser = Parser(PY_LANGUAGE)
|
|
self.normalized_language = PY_LANGUAGE
|
|
self.language_name = "python"
|
|
logger.info(f"Parser is installed to {self.language_name}")
|
|
|
|
def node_text(self, node, source: bytes) -> str:
|
|
return source[node.start_byte : node.end_byte].decode(
|
|
"utf-8",
|
|
errors="replace",
|
|
)
|
|
|
|
def final_callee_name(self, callee) -> str:
|
|
"""Return the last named component of a call expression.
|
|
|
|
Selecting this from the syntax tree is more reliable than splitting
|
|
source text: template arguments (``obj.foo<int>()``) and Python
|
|
subscriptions (``obj.foo[T]()``) may themselves contain ``.`` or
|
|
``::``.
|
|
"""
|
|
# These are grammar field names used by Python attributes/subscripts
|
|
# and C++ field expressions, qualified identifiers, and templates.
|
|
for field_name in ("attribute", "field", "name", "value", "argument"):
|
|
child = callee.child_by_field_name(field_name)
|
|
if child is not None:
|
|
return self.final_callee_name(child)
|
|
return self.node_text(callee, self.source_code)
|
|
|
|
def get_usages(self, function: str):
|
|
query_source: str = ""
|
|
if self.language_name == "python":
|
|
query_source = """
|
|
(call
|
|
function: (_) @callee
|
|
arguments: (argument_list) @arguments
|
|
) @call
|
|
"""
|
|
elif self.language_name == "cpp":
|
|
query_source = """
|
|
(call_expression
|
|
function: (_) @callee
|
|
arguments: (argument_list) @arguments
|
|
) @call
|
|
"""
|
|
|
|
query = Query(
|
|
self.normalized_language,
|
|
query_source,
|
|
)
|
|
|
|
cursor = QueryCursor(query)
|
|
found = False
|
|
for _, captures in cursor.matches(self.tree.root_node):
|
|
call = captures["call"][0]
|
|
callee = captures["callee"][0]
|
|
arguments = captures["arguments"][0]
|
|
|
|
callee_text = self.node_text(callee, self.source_code)
|
|
|
|
final_name = self.final_callee_name(callee)
|
|
|
|
if function not in {callee_text, final_name}:
|
|
continue
|
|
|
|
found = True
|
|
|
|
logger.info(
|
|
"Call found: %s",
|
|
{
|
|
"call": self.node_text(call, self.source_code),
|
|
"callee": callee_text,
|
|
"arguments": self.node_text(
|
|
arguments,
|
|
self.source_code,
|
|
),
|
|
"line": call.start_point.row + 1,
|
|
"column": call.start_point.column + 1,
|
|
},
|
|
)
|
|
|
|
if not found:
|
|
logger.info("No usages found for %r", function)
|
|
|
|
def find_field_values(self, node_type: str, field_name: str):
|
|
"""Find the value of ``field_name`` on every node of ``node_type``.
|
|
|
|
Unlike a query capture, this demonstrates navigating a grammar field
|
|
directly with Node.child_by_field_name().
|
|
"""
|
|
found = False
|
|
nodes = [self.tree.root_node]
|
|
|
|
while nodes:
|
|
node = nodes.pop()
|
|
nodes.extend(reversed(node.children))
|
|
|
|
if node.type != node_type:
|
|
continue
|
|
|
|
field_value = node.child_by_field_name(field_name)
|
|
if field_value is None:
|
|
continue
|
|
|
|
found = True
|
|
logger.info(
|
|
"%s.%s: %s (line %d, column %d)",
|
|
node_type,
|
|
field_name,
|
|
self.node_text(field_value, self.source_code),
|
|
field_value.start_point.row + 1,
|
|
field_value.start_point.column + 1,
|
|
)
|
|
|
|
if not found:
|
|
logger.info(
|
|
"No %r fields found on %r nodes",
|
|
field_name,
|
|
node_type,
|
|
)
|
|
|
|
def read_file(self, filename: str):
|
|
with open(filename, "rb") as file:
|
|
self.source_code = file.read()
|
|
self.tree = self.parser.parse(self.source_code)
|
|
|
|
def dump_tree(self):
|
|
with tempfile.TemporaryFile(mode="w+b", buffering=0) as output:
|
|
self.tree.print_dot_graph(output)
|
|
|
|
output.seek(0)
|
|
graph = output.read().decode("utf-8", errors="replace")
|
|
|
|
logger.info("Tree graph:\n%s", graph.rstrip())
|
|
|
|
def run_command(self, command: str):
|
|
global arg_parser
|
|
args = arg_parser.parse_args()
|
|
if command == "simple_file":
|
|
self.read_file(args.file)
|
|
self.dump_tree()
|
|
tokens = command.split()
|
|
if tokens[0] == "read_file":
|
|
logger.debug(f"{tokens[0]} called with {tokens[1]}")
|
|
self.read_file(tokens[1])
|
|
if tokens[0] == "dump_tree":
|
|
self.dump_tree()
|
|
if tokens[0] == "get_usages":
|
|
self.get_usages(tokens[1])
|
|
if tokens[0] == "find_field_values":
|
|
self.find_field_values(tokens[1], tokens[2])
|
|
|
|
def get_commands(self) -> dict[str, Any]:
|
|
return {
|
|
"simple_file": {},
|
|
"read_file": {},
|
|
"dump_tree": {},
|
|
"get_usages": {},
|
|
"find_field_values": {},
|
|
}
|
|
|
|
def get_positional_path_args(self) -> dict[str, set[int]]:
|
|
return {"read_file": {0}}
|
|
|
|
def get_path_options(self) -> set[str]:
|
|
return {
|
|
"read_file",
|
|
}
|
|
|
|
|
|
def simple_file():
|
|
global arg_parser
|
|
args = arg_parser.parse_args()
|
|
|
|
tree_parser = SingleFileParser(args)
|
|
tree_parser.read_file(args.file)
|
|
tree_parser.dump_tree()
|