# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import string
import sys
from os
import path
import yaml
###############################################################################
# Language-agnostic functionality #
###############################################################################
template_header = (
"/* This file was autogenerated by "
"toolkit/crashreporter/annotations/generate.py. DO NOT EDIT */\n\n"
)
def read_template(template_filename):
"""Read the contents of the template.
If an error
is encountered quit the program.
"""
try:
with open(template_filename)
as template_file:
template = template_file.read()
except OSError
as ex:
sys.exit(
"Error when reading " + template_filename +
":\n" + str(ex) +
"\n")
return template
def extract_crash_scope_list(annotations, scope):
"""Extract an array holding the names of the annotations allowed for
inclusion
in a crash scope.
"""
return [
name
for (name, data)
in annotations
if data.get(
"scope",
"client") == scope
]
def extract_skiplist(annotations):
"""Extract an array holding the names of the annotations that should be
skipped
and the values which will cause them to be skipped.
"""
return [
(name, data.get(
"skip_if"))
for (name, data)
in annotations
if len(data.get(
"skip_if",
"")) >
0
]
def type_to_enum(annotation_type):
"""Emit the enum value corresponding to each annotation type."""
if annotation_type ==
"string":
return "String"
elif annotation_type ==
"boolean":
return "Boolean"
elif annotation_type ==
"u32":
return "U32"
elif annotation_type ==
"u64":
return "U64"
elif annotation_type ==
"usize":
return "USize"
elif annotation_type ==
"object":
return "Object"
def extract_types(annotations):
"""Extract an array holding the type of each annotation."""
return [type_to_enum(data.get(
"type"))
for (_, data)
in annotations]
generators = {}
def content_generator(*extensions):
"""Create a function that generates the content for a file extension."""
def f(func):
for e
in extensions:
generators[e] = func
return f
###############################################################################
# C++ code generation #
###############################################################################
def generate_strings(annotations):
"""Generate strings corresponding to every annotation."""
names = [
' "' + data.get(
"altname", name) +
'"' for (name, data)
in annotations]
return ",\n".join(names)
def generate_enum(annotations):
"""Generate the C++ typed enum holding all the annotations and return it
as a string.
"""
enum =
""
for i, (name, _)
in enumerate(annotations):
enum +=
" " + name +
" = " + str(i) +
",\n"
enum +=
" Count = " + str(len(annotations))
return enum
def generate_annotations_array_initializer(contents):
"""Generates the initializer for a C++ array of annotations."""
initializer = [
" Annotation::" + name
for name
in contents]
return ",\n".join(initializer)
def generate_skiplist_initializer(contents):
"""Generates the initializer for a C++ array of AnnotationSkipValue structs."""
initializer = [
" { Annotation::" + name +
', "' + value +
'" }' for (name, value)
in contents
]
return ",\n".join(initializer)
def generate_types_initializer(contents):
"""Generates the initializer for a C++ array of AnnotationType values."""
initializer = [
" AnnotationType::" + typename
for typename
in contents]
return ",\n".join(initializer)
@content_generator(
"h")
def emit_header(annotations, _output_name):
pingallowedlist = extract_crash_scope_list(annotations,
"ping")
reportallowedlist = extract_crash_scope_list(annotations,
"report")
skiplist = extract_skiplist(annotations)
typelist = extract_types(annotations)
return {
"enum": generate_enum(annotations),
"strings": generate_strings(annotations),
"pingallowedlist": generate_annotations_array_initializer(pingallowedlist),
"reportallowedlist": generate_annotations_array_initializer(reportallowedlist),
"skiplist": generate_skiplist_initializer(skiplist),
"types": generate_types_initializer(typelist),
}
###############################################################################
# Java/Kotlin code generation #
###############################################################################
def javadoc_sanitize(s):
return (
s
.replace(
"<",
"<")
.replace(
">",
">")
.replace(
"@",
"@")
# Kotlin supports nested comments, so change anything that looks like the start of a block comment.
.replace(
"/*",
"/*")
)
def derive_package_and_class(file_path):
"""
Determine the appropriate package
and class name
for a file path,
and
return whether a kotlin source should be generated rather than java.
"""
path = file_path.split(
"src/main/java/",
1)[
1]
package_path, klass_path = path.rsplit(
"/",
1)
package = package_path.replace(
"/",
".")
is_kotlin = klass_path.endswith(
".kt")
klass = klass_path.removesuffix(
".kt").removesuffix(
".java")
return package, klass, is_kotlin
@content_generator(
"java",
"kt")
def emit_java(annotations, output_name):
package, klass, is_kotlin = derive_package_and_class(output_name)
enum =
",\n".join(
f
'/** {javadoc_sanitize(data["description"])} */\n{name}("{name}", "{data.get("scope", "client")}")'
for (name, data)
in annotations
)
return {
"package": package,
"enum": enum,
"class": klass,
}
# Main script entrypoint for GeneratedFile
def main(output, annotations_path):
with open(annotations_path)
as annotations_file:
annotations = yaml.safe_load(annotations_file)
suffix = output.name.rpartition(
".")[
2]
generator = generators.get(suffix)
if generator
is None:
sys.exit(f
"No generator for .{suffix} files")
template_file = f
"CrashAnnotations.{suffix}.in"
template_path = path.join(path.dirname(__file__), template_file)
template_args = generator(annotations, output.name)
content = template_header + string.Template(
read_template(template_path)
).substitute(template_args)
try:
output.write(content)
except OSError
as ex:
sys.exit(
"Error while writing out the generated file:\n" + str(ex) +
"\n")
return {template_path}