mirror of
				https://github.com/tc39/test262.git
				synced 2025-11-04 05:33:50 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			231 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			8.0 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)
 | 
						|
 | 
						|
 | 
						|
def without_duplicates(iterable):
 | 
						|
    return list(OrderedDict.fromkeys(iterable))
 | 
						|
 | 
						|
 | 
						|
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:
 | 
						|
                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:
 | 
						|
            region_name = region['name']
 | 
						|
            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')
 | 
						|
 | 
						|
            if "codepoints" in context['region_options'].get(region_name, set()):
 | 
						|
                str_from_cp = chr
 | 
						|
                try:
 | 
						|
                    # In Python 2, explicitly work on code points
 | 
						|
                    str_from_cp = unichr
 | 
						|
                except NameError: pass
 | 
						|
                value = "".join(str_from_cp(int(cp, 16)) for cp in value.split())
 | 
						|
            else:
 | 
						|
                value = indent(value, whitespace, True).lstrip()
 | 
						|
 | 
						|
            source = source[:region['firstchar']] + \
 | 
						|
                value + \
 | 
						|
                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 = without_duplicates(features)
 | 
						|
        if len(features):
 | 
						|
            lines += ['features: ' + yaml.dump(features,
 | 
						|
                      default_flow_style=True, width=float('inf')).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 = without_duplicates(flags)
 | 
						|
        if (negative and negative.get('phase') == 'parse'
 | 
						|
                and negative.get('type') == 'SyntaxError'
 | 
						|
                and 'async' in flags):
 | 
						|
            flags.remove('async')
 | 
						|
        lines += ['flags: ' + yaml.dump(flags, default_flow_style=True,
 | 
						|
                  width=float('inf')).strip()]
 | 
						|
 | 
						|
        includes = []
 | 
						|
        includes += case_values['meta'].get('includes', [])
 | 
						|
        includes += self.attribs['meta'].get('includes', [])
 | 
						|
        includes = without_duplicates(includes)
 | 
						|
        if len(includes):
 | 
						|
            lines += ['includes: ' + yaml.dump(includes,
 | 
						|
                      default_flow_style=True, width=float('inf')).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):
 | 
						|
        assert encoding == 'utf-8'
 | 
						|
        def get_source():
 | 
						|
            frontmatter = self._frontmatter(case_filename, case_values)
 | 
						|
            body = self.expand_regions(self.source, case_values)
 | 
						|
            return codecs.encode(frontmatter + '\n' + body, encoding)
 | 
						|
        return Test(self.attribs['meta']['path'] + case_name + '.js',
 | 
						|
            dynamic_source=get_source,
 | 
						|
            source_file_names=(self.filename, case_filename))
 |