Skip to content

Commit 1356bd9

Browse files
committed
Add script to add overlay annotations
1 parent 164cfaf commit 1356bd9

File tree

1 file changed

+168
-0
lines changed

1 file changed

+168
-0
lines changed

config/add-overlay-annotations.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# This script is used to annotate .qll files without any existing overlay annotations
2+
# with overlay[local?] and overlay[caller] annotations. Maintenance of overlay annotations
3+
# in annotated files will be handled by QL-for-QL queries.
4+
5+
# It will walk the directory tree and annotate most .qll files, skipping only
6+
# some specific cases (e.g., empty files, files that configure dataflow for queries).
7+
8+
# The script takes a list of languages and processes the corresponding directories.
9+
10+
# Usage: python3 add-overlay-annotations.py <language1> <language2> ...
11+
12+
# The script will modify the files in place and print the changes made.
13+
# The script is designed to be run from the root of the repository.
14+
15+
#!/usr/bin/python3
16+
import sys
17+
import os
18+
from difflib import *
19+
20+
21+
def has_overlay_annotations(lines):
22+
'''
23+
Check whether the given lines contain any overlay[...] annotations.
24+
'''
25+
overlays = ["local", "local?", "global", "caller"]
26+
annotations = [f"overlay[{t}]" for t in overlays]
27+
return any(any(ann in line for ann in annotations) for line in lines)
28+
29+
30+
def insert_toplevel_maybe_local_anntotation(filename, lines):
31+
'''
32+
Find a suitable place to insert an overlay[local?] annotation at the top of the file.
33+
Return a pair: (string describing action taken, modified content as list of lines).
34+
'''
35+
out_lines = []
36+
status = 0
37+
38+
for line in lines:
39+
if status == 0 and line.rstrip().endswith("module;"):
40+
out_lines.append("overlay[local?]\n")
41+
status = 1
42+
out_lines.append(line)
43+
44+
if status == 1:
45+
return (f"Annotating \"{filename}\" via existing file-level module statement", out_lines)
46+
47+
out_lines = []
48+
empty_line_buffer = []
49+
status = 0
50+
for line in lines:
51+
trimmed = line.strip()
52+
if not trimmed:
53+
empty_line_buffer.append(line)
54+
continue
55+
if status <= 1 and trimmed.endswith("*/"):
56+
status = 2
57+
elif status == 0 and trimmed.startswith("/**"):
58+
status = 1
59+
elif status == 0 and not trimmed.startswith("/*"):
60+
out_lines.append("overlay[local?]\n")
61+
out_lines.append("module;\n")
62+
out_lines.append("\n")
63+
status = 3
64+
elif status == 2 and (trimmed.startswith("import ") or trimmed.startswith("private import ")):
65+
out_lines.append("overlay[local?]\n")
66+
out_lines.append("module;\n")
67+
status = 3
68+
elif status == 2 and (trimmed.startswith("class ") or trimmed.startswith("predicate ")
69+
or trimmed.startswith("module ") or trimmed.startswith("signature ")):
70+
out_lines = ["overlay[local?]\n", "module;\n", "\n"] + out_lines
71+
status = 3
72+
elif status == 2 and trimmed.startswith("/*"):
73+
out_lines.append("overlay[local?]\n")
74+
out_lines.append("module;\n")
75+
status = 3
76+
elif status == 2:
77+
status = 4
78+
if empty_line_buffer:
79+
out_lines += empty_line_buffer
80+
empty_line_buffer = []
81+
out_lines.append(line)
82+
if status == 3:
83+
out_lines += empty_line_buffer
84+
85+
if status == 3:
86+
return (f"Annotating \"{filename}\" after file-level module qldoc", out_lines)
87+
88+
raise Exception(f"Failed to annotate \"{filename}\" as overlay[local?].")
89+
90+
91+
def insert_overlay_caller_annotations(lines):
92+
'''
93+
Mark pragma[inline] predicates as overlay[caller] if they are not declared private.
94+
'''
95+
out_lines = []
96+
for i, line in enumerate(lines):
97+
trimmed = line.strip()
98+
if trimmed == "pragma[inline]":
99+
if (not "private" in lines[i+1]):
100+
whitespace = line[0: line.find(trimmed)]
101+
out_lines.append(f"{whitespace}overlay[caller]\n")
102+
out_lines.append(line)
103+
return out_lines
104+
105+
106+
def annotate_as_appropriate(filename, lines):
107+
'''
108+
Insert new overlay[...] annotations according to heuristics in files without existing
109+
overlay annotations.
110+
111+
Returns None if no annotations are needed. Otherwise, returns a pair consisting of a
112+
string describing the action taken and the modified content as a list of lines.
113+
'''
114+
if has_overlay_annotations(lines):
115+
return None
116+
117+
# These simple heuristics filter out those .qll files that we no _not_ want to annotate
118+
# as overlay[local?]. It is not clear that these heuristics are exactly what we want,
119+
# but they seem to work well enough for now (as determined by speed and accuracy numbers).
120+
if (filename.endswith("Test.qll") or
121+
((filename.endswith("Query.qll") or filename.endswith("Config.qll")) and
122+
any("implements DataFlow::ConfigSig" in line for line in lines))):
123+
return None
124+
elif not any(line for line in lines if line.strip()):
125+
return None
126+
127+
lines = insert_overlay_caller_annotations(lines)
128+
return insert_toplevel_maybe_local_anntotation(filename, lines)
129+
130+
131+
def process_single_file(filename):
132+
'''
133+
Process a single file, annotating it as appropriate and writing the changes back to the file.
134+
'''
135+
old = [line for line in open(filename)]
136+
137+
annotate_result = annotate_as_appropriate(filename, old)
138+
if annotate_result is None:
139+
return
140+
141+
new = annotate_result[1]
142+
143+
diff = context_diff(old, new, fromfile=filename, tofile=filename)
144+
diff = [line for line in diff]
145+
if diff:
146+
print(annotate_result[0])
147+
for line in diff:
148+
print(line.rstrip())
149+
with open(filename, "w") as out_file:
150+
for line in new:
151+
out_file.write(line)
152+
153+
154+
dirs = []
155+
for lang in sys.argv[1:]:
156+
if lang in ["cpp", "go", "csharp", "java", "javascript", "python", "ruby", "rust", "swift"]:
157+
dirs.append(f"{lang}/ql/lib")
158+
else:
159+
raise Exception(f"Unknown language \"{lang}\".")
160+
161+
if dirs:
162+
dirs.append("shared")
163+
164+
for roots in dirs:
165+
for dirpath, dirnames, filenames in os.walk(roots):
166+
for filename in filenames:
167+
if filename.endswith(".qll") and not dirpath.endswith("tutorial"):
168+
process_single_file(os.path.join(dirpath, filename))

0 commit comments

Comments
 (0)