#!/usr/bin/env vpython3
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Script to extract edits from rewrite_templated_container_fields clang
tool output.
If the tool emits edits, then the edits should look like this:
...
{lhs_node1};{lhs_node2}
{node_n}
{lhs_node3};{lhs_node4}
...
...
Where lhs_node, rhs_node, and node_n represent a node's text representation
generated using rewrite_templated_container_fields' Node::ToString() function.
The string representation has the following format:
`{is_field\,is_excluded\,has_auto_type\,r:::<file path>:::<offset>:::<length>
:::<replacement text>\,include-user-header:::<file path>:::-1:::-1
:::<include text>}`
where `is_field`,`is_excluded`, and `has_auto_type` are booleans represendted
as 0 or 1.
extract_edits.py takes input that is concatenated from multiple tool
invocations and extract just the edits with the following steps:
1- Construct the adjacency list of nodes
(a pairs of nodes represents an edge in the graph)
2- Run `PropagateExclusions` to exclude fields reachable
from a RAW_PTR_EXCLUSION annotated field.
3- Run `DFS` starting from non-excluded nodes and emit
edtis for reachable nodes.
extract_edits.py would then emit the following output:
<edit1>
<edit2>
<edit3>
...
Where the edit is either a replacemnt or an include directive.
For more details about how the tool works, see the doc here:
https://docs.google.com/document/d/1P8wLVS3xueI4p3EAPO4JJP6d1_zVp5SapQB0EW9iHQI/
"""
from __future__ import print_function
from collections import defaultdict
import sys
class Node:
is_field = "0"
is_excluded = "0"
has_auto_type = "0"
replacement = ""
include_directive = ""
neighbors = set()
def __init__(self, is_field, is_excluded, has_auto_type, replacement,
include_directive) -> None:
self.is_field = is_field
self.is_excluded = is_excluded
self.replacement = replacement
self.has_auto_type = has_auto_type
self.include_directive = include_directive
self.neighbors = set()
def __eq__(self, other):
if isinstance(other, Node):
return self.replacement == other.replacement
return False
def __hash__(self) -> int:
return hash((self.replacement, self.include_directive))
def GetNode(txt: str):
txt = txt[1:len(txt) - 1]
x = txt.split('\\,')
return Node(x[0], x[1], x[2], x[3], x[4])
def DFS(visited: set, graph: defaultdict, key: str, key_to_node: defaultdict,
changes: set):
if key not in visited:
node = key_to_node[key]
if node.has_auto_type == "0":
changes.add(node.replacement)
changes.add(node.include_directive)
visited.add(key)
for neighbour in graph[key]:
DFS(visited, graph, neighbour.replacement, key_to_node, changes)
# to propagate field exclusions to all neighbors
def PropagateExclusions(visited: set, graph: defaultdict, key: str,
key_to_node: defaultdict):
if key not in visited:
n = key_to_node[key]
n.is_excluded = "1"
key_to_node[key] = n
visited.add(key)
for neighbour in graph[key]:
PropagateExclusions(visited, graph, neighbour.replacement, key_to_node)
def main():
graph = defaultdict()
key_to_node = defaultdict() # since we cannot use nodes as keys
# to map, use this to map node replacemnt to node.
inside_marker_lines = False
changes = set()
excluded_fields = set()
for line in sys.stdin:
line = line.rstrip("\n\r")
if line == '==== BEGIN EDITS ====':
inside_marker_lines = True
continue
if line == '==== END EDITS ====':
inside_marker_lines = False
continue
if inside_marker_lines:
changes.add(line)
continue
ar = line.split(";")
# These are fieldDecls
if len(ar) == 1:
lhs = GetNode(ar[0])
# if the field is annotated with RAW_PTR_EXCLUSION,
# add it to the set of excluded fields
# this will be later propagated to all neighboring fields.
if lhs.is_excluded == "1":
excluded_fields.add(lhs.replacement)
lhs.is_excluded = "0"
key_to_node[lhs.replacement] = lhs
if lhs.replacement not in graph:
graph.setdefault(lhs.replacement, set())
continue
lhs = GetNode(ar[0])
rhs = GetNode(ar[1])
# In the case of a typedefNameDecl, all the var/param/fields
# end up creating the same replacement. What is being done here is
# that if any field has a typedefNameDecl type, make all matches
# current and previous marked as is_field
if lhs.replacement in key_to_node.keys():
lhs.is_field = "1" if lhs.is_field == "1" or key_to_node[
lhs.replacement].is_field == "1" else "0"
if rhs.replacement in key_to_node.keys():
rhs.is_field = "1" if rhs.is_field == "1" or key_to_node[
rhs.replacement].is_field == "1" else "0"
key_to_node[lhs.replacement] = lhs
key_to_node[rhs.replacement] = rhs
if lhs.replacement not in graph:
graph.setdefault(lhs.replacement, set())
graph[lhs.replacement].add(rhs)
if rhs.replacement not in graph:
graph.setdefault(rhs.replacement, set())
graph[rhs.replacement].add(lhs)
# Propagate changes to all excluded fields
visited = set()
for key in excluded_fields:
key_to_node[key].is_excluded = "1"
PropagateExclusions(visited, graph, key, key_to_node)
visited = set()
for key in graph.keys():
node = key_to_node[key]
if node.is_field == "1" and node.is_excluded == "0" and key not in visited:
DFS(visited, graph, key, key_to_node, changes)
changes = sorted(changes)
for text in changes:
print(text)
return 0
if __name__ == '__main__':
sys.exit(main())