-
Notifications
You must be signed in to change notification settings - Fork 64
/
Copy pathcoding_standards_utils.py
165 lines (136 loc) · 7.12 KB
/
coding_standards_utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import re
from typing import Type, Dict, List, Optional, TextIO, Generator, Union, Pattern
from jinja2 import Template
from pathlib import Path
from dataclasses import dataclass
import marko
from marko.md_renderer import MarkdownRenderer
import tempfile
import sys
# Add the shared module to the path
script_path = Path(__file__)
sys.path.append(str(script_path.parent.parent / 'shared'))
from codeql import CodeQL, CodeQLError
from markdown_helpers import HeadingFormatUpdateSpec, update_help_file, HeadingDiffUpdateSpec
# Global holding an instance of CodeQL that can be shared too prevent repeated instantiation costs.
codeql = None
def split_camel_case(short_name : str) -> List[str]:
"""Split a camel case string to a list."""
# Edge case, turn FooNaNBar into foo-nan-bar instead of foo-na-n-bar by a preprocessing step.
nan_fixed = short_name.replace("NaN", "Nan")
matches = re.finditer(
".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", nan_fixed)
return [m.group(0) for m in matches]
def standard_tag(standard_short_name : str, key : str, value: Optional[str]) -> str:
"""Create a CodeQL tag for the given standard and property name"""
return (
"external/"
+ standard_short_name
+ "/"
+ key.replace("_", "-").replace(" ", "-").lower()
+ (("/" + value.replace("_", "-").replace(" ", "-").lower()) if value else "")
)
def description_line_break(description : str) -> List[str]:
"""Split the description into a list of lines of no more than 84 characters"""
if len(description) < 85:
return [description]
else:
description_line = description[0:85]
# Reports the last space, or -1
last_space = description_line.rfind(" ")
# If last space is -1, then we lose the extra character as required
description_line = description_line[0:last_space]
# Use recursion to calculate the lines in the tail of the string
return [description_line] + description_line_break(description[(len(description_line) + 1):])
def write_template(template: Type[Template], args: Dict[str, str], package_name: str, file: Type[Path]) -> None:
"""Render the template with the given args, and write it to the file using \n newlines."""
with open(file, "w", newline="\n") as f:
render_template(template, args, package_name, f)
def render_template(template: Type[Template], args: Dict[str, str], package_name: str, file: TextIO) -> None:
output = template.render(args, package_name=package_name)
file.write(output)
def write_exclusion_template(template: Type[Template], args: Dict[str, str], package_name: str, language_name: str, file: TextIO):
"""Render the template with the given args, and write it to the file using \n newlines."""
output = template.render(
data=args, package_name=package_name, language_name=language_name)
with open(file, "w", newline="\n") as f:
f.write(output)
global codeql
if codeql == None:
codeql = CodeQL()
# Format the generated exclusion file because we don't want to handle this in the template.
# The format relies on the length of the package name.
codeql.format(file)
def extract_metadata_from_query(rule_id, title, rule_category, q, rule_query_tags, language_name, ql_language_name, standard_name, standard_short_name, standard_metadata, anonymise):
metadata = q.copy()
# Add rule properties to the query dict, ready for template generation
metadata["standard_name"] = standard_name
metadata["standard_short_name"] = standard_short_name
metadata["rule_id"] = rule_id
metadata["rule_title"] = title
metadata["tags"].extend(rule_query_tags)
metadata["language_name"] = language_name
metadata["ql_language_name"] = ql_language_name
metadata["shortname_camelcase"] = metadata["short_name"][0].lower(
) + metadata["short_name"][1:]
# short id generated from short name
metadata["short_id"] = "-".join([c.lower()
for c in split_camel_case(metadata["short_name"])])
# set up model for exclusions
exclusion_model = {}
# queryid currently assumes c or c++ queries
exclusion_model["queryid"] = f"{language_name}/{standard_short_name}/{metadata['short_id']}"
exclusion_model["ruleid"] = rule_id
exclusion_model["queryname"] = metadata["short_name"]
exclusion_model["queryname_camelcase"] = metadata["short_name"][0].lower(
) + metadata["short_name"][1:]
exclusion_model["category"] = rule_category
if not "kind" in metadata:
# default to problem if not specified
metadata["kind"] = "problem"
# separate description into multiple lines
metadata["description"] = description_line_break(
metadata["description"] if "description" in metadata else metadata["rule_title"])
# Anonymise properties, if required
if anonymise:
metadata["rule_title"] = metadata["short_name"]
metadata["description"] = [metadata["short_name"]
] if anonymise else metadata["description"]
metadata["name"] = metadata["short_name"]
if not standard_name in standard_metadata:
exit(f"Unsupported standard '{standard_name}'")
metadata.update(standard_metadata[standard_name])
return metadata, exclusion_model
def write_query_help_file(help_dir: Type[Path], env: any, query: any, package_name: str, rule_id: str, standard_name: str) -> None:
# Add Markdown help file, partially overwriting elements that are automatically generated.
help_path = help_dir.joinpath(
query["short_name"] + ".md")
help_template = env.get_template(f"{standard_name.lower()}-help.md.template")
if not help_path.exists():
print(
rule_id + ": Writing out query help file to " + str(help_path))
write_template(help_template, query,
package_name, help_path)
else:
print(rule_id + ": Updating query help file at " + str(help_path))
with tempfile.TemporaryFile(mode="r+", suffix="md") as updated_help_fd:
render_template(
help_template, query, package_name, updated_help_fd)
updated_help_fd.seek(0)
md = marko.Markdown(renderer=MarkdownRenderer)
updated_help = md.parse(updated_help_fd.read())
with help_path.open(mode="r+") as existing_help_fd:
existing_help = md.parse(
existing_help_fd.read())
updates = [HeadingDiffUpdateSpec("Implementation notes", updated_help),
HeadingDiffUpdateSpec(
"References", updated_help),
HeadingDiffUpdateSpec(re.compile(
f"^{rule_id}\\s*:"), updated_help),
HeadingFormatUpdateSpec()
]
update_help_file(existing_help, updates)
existing_help_fd.seek(0)
existing_help_fd.truncate(0)
existing_help_fd.write(
md.render(existing_help))