test262/tools/generation/lib/template.py

211 lines
7.3 KiB
Python

# Copyright (C) 2016 the V8 project authors. All rights reserved.
# This code is governed by the BSD license found in the LICENSE file.
import os, re
import codecs, yaml
from collections import OrderedDict
from .util.find_comments import find_comments
from .util.parse_yaml import parse_yaml
from .test import Test
indentPattern = re.compile(r'^(\s*)')
interpolatePattern = re.compile(r'\{\s*(\S+)\s*\}')
def indent(text, prefix = ' ', js_value = False):
'''Prefix a block of text (as defined by the "line break" control
character) with some character sequence.
:param prefix: String value to insert before each line
:param js_value: If True, the text will be interpreted as a JavaScript
value, meaning that indentation will not occur for lines that would
effect the runtime value; defaults to False
'''
if isinstance(text, list):
lines = text
else:
lines = text.split('\n')
indented = [prefix + lines[0]]
str_char = None
for line in lines[1:]:
# Determine if the beginning of the current line is part of some
# previously-opened literal value.
if js_value:
for char in indented[-1]:
if char == str_char:
str_char = None
elif str_char is None and char in '\'"`':
str_char = char
# Do not indent the current line if it is a continuation of a literal
# value or if it is empty.
if str_char or len(line) == 0:
indented.append(line)
else:
indented.append(prefix + line)
return '\n'.join(indented)
class Template:
def __init__(self, filename, encoding):
self.filename = filename
with codecs.open(filename, 'r', encoding) as template_file:
self.source = template_file.read()
self.attribs = dict()
self.regions = []
self._parse()
def _remove_comment(self, comment):
'''Create a region that is not intended to be referenced by any case,
ensuring that the comment is not emitted in the rendered file.'''
name = '__remove_comment_' + str(comment['firstchar']) + '__'
# When a removed comment ends the line, the following newline character
# should also be removed from the generated file.
lastchar = comment['lastchar']
if self.source[lastchar] == '\n':
comment['lastchar'] = comment['lastchar'] + 1
self.regions.insert(0, dict(name=name, **comment))
def _parse(self):
for comment in find_comments(self.source):
meta = parse_yaml(comment['source'])
# Do not emit the template's frontmatter in generated files
# (file-specific frontmatter is generated as part of the rendering
# process)
if meta:
self.attribs['meta'] = meta
self._remove_comment(comment)
continue
# Do not emit license information in generated files (recognized as
# comments preceeding the YAML frontmatter)
if not self.attribs.get('meta'):
self._remove_comment(comment)
continue
match = interpolatePattern.match(comment['source'])
if match == None:
continue
self.regions.insert(0, dict(name=match.group(1), **comment))
def expand_regions(self, source, context):
lines = source.split('\n')
for region in self.regions:
whitespace = indentPattern.match(lines[region['lineno']]).group(1)
value = context['regions'].get(region['name'], '')
str_char = region.get('in_string')
if str_char:
safe_char = '"' if str_char == '\'' else '\''
value = value.replace(str_char, safe_char)
value = value.replace('\n', '\\\n')
source = source[:region['firstchar']] + \
indent(value, whitespace, True).lstrip() + \
source[region['lastchar']:]
setup = context['regions'].get('setup')
if setup:
source = setup + '\n' + source
teardown = context['regions'].get('teardown')
if teardown:
source += '\n' + teardown + '\n'
return source
def _frontmatter(self, case_filename, case_values):
description = case_values['meta']['desc'].strip() + \
' (' + self.attribs['meta']['name'].strip() + ')'
lines = []
lines += [
'// This file was procedurally generated from the following sources:',
'// - ' + case_filename,
'// - ' + self.filename,
'/*---',
'description: ' + description,
]
esid = self.attribs['meta'].get('esid')
if esid:
lines.append('esid: ' + esid)
es6id = self.attribs['meta'].get('es6id')
if es6id:
lines.append('es6id: ' + es6id)
features = []
features += case_values['meta'].get('features', [])
features += self.attribs['meta'].get('features', [])
features = list(OrderedDict.fromkeys(features))
if len(features):
lines += ['features: ' + re.sub('\n\s*', ' ', yaml.dump(features, default_flow_style=True).strip())]
# Reconcile "negative" meta data before "flags"
if case_values['meta'].get('negative'):
if self.attribs['meta'].get('negative'):
raise Exception('Cannot specify negative in case and template file')
negative = case_values['meta'].get('negative')
else:
negative = self.attribs['meta'].get('negative')
flags = ['generated']
flags += case_values['meta'].get('flags', [])
flags += self.attribs['meta'].get('flags', [])
flags = list(OrderedDict.fromkeys(flags))
if 'async' in flags and negative and negative.get('phase') == 'parse' and negative.get('type') == 'SyntaxError':
flags.remove('async')
lines += ['flags: ' + re.sub('\n\s*', ' ', yaml.dump(flags, default_flow_style=True).strip())]
includes = []
includes += case_values['meta'].get('includes', [])
includes += self.attribs['meta'].get('includes', [])
includes = list(OrderedDict.fromkeys(includes))
if len(includes):
lines += ['includes: ' + re.sub('\n\s*', ' ', yaml.dump(includes, default_flow_style=True).strip())]
if negative:
lines += ['negative:']
as_yaml = yaml.dump(negative, default_flow_style=False)
lines += indent(as_yaml.strip(), ' ').split('\n')
info = []
if 'info' in self.attribs['meta']:
info.append(indent(self.attribs['meta']['info']))
if 'info' in case_values['meta']:
if len(info):
info.append('')
info.append(indent(case_values['meta']['info']))
if len(info):
lines.append('info: |')
lines += info
lines.append('---*/')
return '\n'.join(lines)
def expand(self, case_filename, case_name, case_values, encoding):
frontmatter = self._frontmatter(case_filename, case_values)
body = self.expand_regions(self.source, case_values)
assert encoding == 'utf-8'
return Test(self.attribs['meta']['path'] + case_name + '.js',
source=codecs.encode(frontmatter + '\n' + body, encoding))