diff --git a/pkg/compiler/lib/src/js/js.dart b/pkg/compiler/lib/src/js/js.dart index 9c8328ee282..b49abc0fad7 100644 --- a/pkg/compiler/lib/src/js/js.dart +++ b/pkg/compiler/lib/src/js/js.dart @@ -157,7 +157,7 @@ class UnparsedNode extends DeferredString implements AstContainer { } } } - _cachedLiteral = js.string(text); + _cachedLiteral = js.escapedString(text); } return _cachedLiteral; } diff --git a/pkg/compiler/lib/src/js/size_estimator.dart b/pkg/compiler/lib/src/js/size_estimator.dart index 24527ec6755..8ecfd9e4799 100644 --- a/pkg/compiler/lib/src/js/size_estimator.dart +++ b/pkg/compiler/lib/src/js/size_estimator.dart @@ -749,24 +749,24 @@ class SizeEstimator implements NodeVisitor { } bool isValidJavaScriptId(String field) { - if (field.length == 0) return false; + if (field.length < 3) return false; // Ignore the leading and trailing string-delimiter. - for (int i = 0; i < field.length; i++) { + for (int i = 1; i < field.length - 1; i++) { // TODO(floitsch): allow more characters. int charCode = field.codeUnitAt(i); if (!(charCodes.$a <= charCode && charCode <= charCodes.$z || charCodes.$A <= charCode && charCode <= charCodes.$Z || charCode == charCodes.$$ || charCode == charCodes.$_ || - i > 0 && isDigit(charCode))) { + i != 1 && isDigit(charCode))) { return false; } } // TODO(floitsch): normally we should also check that the field is not a // reserved word. We don't generate fields with reserved word names except // for 'super'. - if (field == 'super') return false; - if (field == 'catch') return false; + if (field == '"super"') return false; + if (field == '"catch"') return false; return true; } @@ -776,17 +776,16 @@ class SizeEstimator implements NodeVisitor { newInForInit: inForInit, newAtStatementBegin: atStatementBegin); Node selector = access.selector; if (selector is LiteralString) { - String field = literalStringToString(selector); - if (isValidJavaScriptId(field)) { + String fieldWithQuotes = literalStringToString(selector); + if (isValidJavaScriptId(fieldWithQuotes)) { if (access.receiver is LiteralNumber) { // We can eliminate the space in some cases, but for simplicity we // always assume it is necessary. out(' '); // ' ' } - // '.${field}' - out('.'); - out(field); + // '.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}' + out('.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}'); return; } } else if (selector is Name) { @@ -876,9 +875,7 @@ class SizeEstimator implements NodeVisitor { @override void visitLiteralString(LiteralString node) { - out('"'); out(literalStringToString(node)); - out('"'); } @override @@ -971,12 +968,10 @@ class SizeEstimator implements NodeVisitor { if (name is LiteralString) { String text = literalStringToString(name); if (isValidJavaScriptId(text)) { - out(text); + // '${text.substring(1, text.length - 1)} + out('${text.substring(1, text.length - 1)}'); } else { - // Approximation to `_handleString(text)`. - out('"'); - out(text); - out('"'); + out(text); // '$text' } } else if (name is Name) { node.name.accept(this); diff --git a/pkg/compiler/lib/src/js_backend/constant_emitter.dart b/pkg/compiler/lib/src/js_backend/constant_emitter.dart index dd67c4aea39..ec3928d6bd5 100644 --- a/pkg/compiler/lib/src/js_backend/constant_emitter.dart +++ b/pkg/compiler/lib/src/js_backend/constant_emitter.dart @@ -148,7 +148,7 @@ class ModularConstantEmitter jsAst.Expression visitString(StringConstantValue constant, [_]) { String value = constant.stringValue; if (value.length < StringReferencePolicy.minimumLength) { - return js.string(value); + return js.escapedString(value, ascii: true); } return StringReference(constant); } @@ -288,7 +288,8 @@ class ConstantEmitter extends ModularConstantEmitter { } // Keys in literal maps must be emitted in place. - jsAst.Literal keyExpression = js.string(key.stringValue); + jsAst.Literal keyExpression = + js.escapedString(key.stringValue, ascii: true); jsAst.Expression valueExpression = _constantReferenceGenerator(constant.values[i]); properties.add(new jsAst.Property(keyExpression, valueExpression)); diff --git a/pkg/compiler/lib/src/js_backend/runtime_types_new.dart b/pkg/compiler/lib/src/js_backend/runtime_types_new.dart index 4c59ced6ccb..3c9f11f75cf 100644 --- a/pkg/compiler/lib/src/js_backend/runtime_types_new.dart +++ b/pkg/compiler/lib/src/js_backend/runtime_types_new.dart @@ -109,7 +109,9 @@ class _RecipeGenerator implements DartTypeVisitor { return _finishEncoding(js.string(String.fromCharCodes(_codes))); } _flushCodes(); - return _finishEncoding(jsAst.StringConcatenation(_fragments)); + jsAst.LiteralString quote = jsAst.LiteralString('"'); + return _finishEncoding( + jsAst.StringConcatenation([quote, ..._fragments, quote])); } void _start(TypeRecipe recipe) { @@ -485,13 +487,13 @@ class RulesetEncoder { CommonElements get _commonElements => _dartTypes.commonElements; ClassEntity get _objectClass => _commonElements.objectClass; - final _leftBrace = js.string('{'); - final _rightBrace = js.string('}'); - final _leftBracket = js.string('['); - final _rightBracket = js.string(']'); - final _colon = js.string(':'); - final _comma = js.string(','); - final _doubleQuote = js.string('"'); + final _leftBrace = js.stringPart('{'); + final _rightBrace = js.stringPart('}'); + final _leftBracket = js.stringPart('['); + final _rightBracket = js.stringPart(']'); + final _colon = js.stringPart(':'); + final _comma = js.stringPart(','); + final _quote = js.stringPart("'"); bool _isObject(InterfaceType type) => identical(type.element, _objectClass); @@ -520,32 +522,28 @@ class RulesetEncoder { jsAst.StringConcatenation _encodeRuleset(Ruleset ruleset) => js.concatenateStrings([ + _quote, _leftBrace, ...js.joinLiterals([ ...ruleset._redirections.entries.map(_encodeRedirection), ...ruleset._entries.entries.map(_encodeEntry), ], _comma), _rightBrace, + _quote, ]); jsAst.StringConcatenation _encodeRedirection( MapEntry redirection) => js.concatenateStrings([ - _doubleQuote, - _emitter.typeAccessNewRti(redirection.key), - _doubleQuote, + js.quoteName(_emitter.typeAccessNewRti(redirection.key)), _colon, - _doubleQuote, - _emitter.typeAccessNewRti(redirection.value), - _doubleQuote, + js.quoteName(_emitter.typeAccessNewRti(redirection.value)), ]); jsAst.StringConcatenation _encodeEntry( MapEntry entry) => js.concatenateStrings([ - _doubleQuote, - _emitter.typeAccessNewRti(entry.key.element), - _doubleQuote, + js.quoteName(_emitter.typeAccessNewRti(entry.key.element)), _colon, _leftBrace, ...js.joinLiterals([ @@ -560,9 +558,7 @@ class RulesetEncoder { jsAst.StringConcatenation _encodeSupertype( InterfaceType targetType, InterfaceType supertype) => js.concatenateStrings([ - _doubleQuote, - _emitter.typeAccessNewRti(supertype.element), - _doubleQuote, + js.quoteName(_emitter.typeAccessNewRti(supertype.element)), _colon, _leftBracket, ...js.joinLiterals( @@ -575,36 +571,30 @@ class RulesetEncoder { jsAst.StringConcatenation _encodeTypeVariable(InterfaceType targetType, TypeVariableType typeVariable, DartType supertypeArgument) => js.concatenateStrings([ - _doubleQuote, - _emitter.typeVariableAccessNewRti(typeVariable.element), - _doubleQuote, + js.quoteName(_emitter.typeVariableAccessNewRti(typeVariable.element)), _colon, _encodeSupertypeArgument(targetType, supertypeArgument), ]); jsAst.Literal _encodeSupertypeArgument( InterfaceType targetType, DartType supertypeArgument) => - js.concatenateStrings([ - _doubleQuote, - _recipeEncoder.encodeMetadataRecipe( - _emitter, targetType, supertypeArgument), - _doubleQuote - ]); + _recipeEncoder.encodeMetadataRecipe( + _emitter, targetType, supertypeArgument); jsAst.StringConcatenation encodeErasedTypes( Map erasedTypes) => js.concatenateStrings([ + _quote, _leftBrace, ...js.joinLiterals(erasedTypes.entries.map(encodeErasedType), _comma), _rightBrace, + _quote, ]); jsAst.StringConcatenation encodeErasedType( MapEntry entry) => js.concatenateStrings([ - _doubleQuote, - _emitter.typeAccessNewRti(entry.key), - _doubleQuote, + js.quoteName(_emitter.typeAccessNewRti(entry.key)), _colon, js.number(entry.value), ]); @@ -612,20 +602,20 @@ class RulesetEncoder { jsAst.StringConcatenation encodeTypeParameterVariances( Map> typeParameterVariances) => js.concatenateStrings([ + _quote, _leftBrace, ...js.joinLiterals( typeParameterVariances.entries .map(_encodeTypeParameterVariancesForClass), _comma), _rightBrace, + _quote, ]); jsAst.StringConcatenation _encodeTypeParameterVariancesForClass( MapEntry> classEntry) => js.concatenateStrings([ - _doubleQuote, - _emitter.typeAccessNewRti(classEntry.key), - _doubleQuote, + js.quoteName(_emitter.typeAccessNewRti(classEntry.key)), _colon, _leftBracket, ...js.joinLiterals( diff --git a/pkg/compiler/lib/src/js_backend/string_reference.dart b/pkg/compiler/lib/src/js_backend/string_reference.dart index 590ff932410..05e1b8f44bd 100644 --- a/pkg/compiler/lib/src/js_backend/string_reference.dart +++ b/pkg/compiler/lib/src/js_backend/string_reference.dart @@ -256,7 +256,8 @@ class StringReferenceFinalizerImpl implements StringReferenceFinalizer { for (_ReferenceSet referenceSet in _referencesByString.values) { if (referenceSet.generateAtUse) { StringConstantValue constant = referenceSet.constant; - js.Expression reference = js.string(constant.stringValue); + js.Expression reference = + js.js.escapedString(constant.stringValue, ascii: true); for (StringReference ref in referenceSet._references) { ref.value = reference; } @@ -274,7 +275,8 @@ class StringReferenceFinalizerImpl implements StringReferenceFinalizer { for (_ReferenceSet referenceSet in referenceSetsUsingProperties) { String string = referenceSet.constant.stringValue; var propertyName = js.string(referenceSet.propertyName); - properties.add(js.Property(propertyName, js.string(string))); + properties.add( + js.Property(propertyName, js.js.escapedString(string, ascii: true))); var access = js.js('#.#', [holderLocalName, propertyName]); for (StringReference ref in referenceSet._references) { ref.value = access; diff --git a/pkg/compiler/test/codegen/jsarray_indexof_test.dart b/pkg/compiler/test/codegen/jsarray_indexof_test.dart index b567137ea6e..4b75ecd56d5 100644 --- a/pkg/compiler/test/codegen/jsarray_indexof_test.dart +++ b/pkg/compiler/test/codegen/jsarray_indexof_test.dart @@ -67,7 +67,7 @@ runTest(List options) async { "${js.nodeToString(method.code, pretty: true)}"); }, onPropertyAccess: (js.PropertyAccess node) { js.Node selector = node.selector; - if (selector is js.LiteralString && selector.value == 'length') { + if (selector is js.LiteralString && selector.value == '"length"') { lengthCount++; } }); diff --git a/pkg/compiler/test/codegen/model_test.dart b/pkg/compiler/test/codegen/model_test.dart index ba7376d4625..f7a1537430e 100644 --- a/pkg/compiler/test/codegen/model_test.dart +++ b/pkg/compiler/test/codegen/model_test.dart @@ -101,7 +101,7 @@ class ModelIrComputer extends IrDataExtractor { /// Call to fixed backend name, so we include the argument /// values to test encoding of optional parameters in native /// methods. - name = selector.value; + name = selector.value.substring(1, selector.value.length - 1); fixedNameCall = true; } if (name != null) { @@ -146,7 +146,7 @@ class ModelIrComputer extends IrDataExtractor { /// Call to fixed backend name, so we include the argument /// values to test encoding of optional parameters in native /// methods. - name = selector.value; + name = selector.value.substring(1, selector.value.length - 1); } if (receiverName != null && name != null) { @@ -236,7 +236,7 @@ class ModelIrComputer extends IrDataExtractor { if (selector is js.Name) { name = selector.key; } else if (selector is js.LiteralString) { - name = selector.value; + name = selector.value.substring(1, selector.value.length - 1); } if (name != null) { features.addElement(Tags.assignment, '${name}'); diff --git a/pkg/compiler/test/js/js_parser_test.dart b/pkg/compiler/test/js/js_parser_test.dart index e9b482206aa..7e7ac3f3028 100644 --- a/pkg/compiler/test/js/js_parser_test.dart +++ b/pkg/compiler/test/js/js_parser_test.dart @@ -20,9 +20,7 @@ testExpression(String expression, [String expect = ""]) { testError(String expression, [String expect = ""]) { bool doCheck(exception) { - final exceptionText = '$exception'; - Expect.isTrue(exceptionText.contains(expect), - 'Missing "$expect" in "$exceptionText"'); + Expect.isTrue(exception.toString().contains(expect)); return true; } @@ -67,9 +65,9 @@ void main() { // String literal with \n. testExpression(r'var x = "\n"'); // String literal with escaped quote. - testExpression(r'''var x = "\""''', r"""var x = '"'"""); + testExpression(r'var x = "\""'); // *No clever escapes. - testError(r'var x = "\x42"', 'Hex escapes not supported'); + testError(r'var x = "\x42"', 'escapes are not allowed in literals'); // Operator new. testExpression('new Foo()'); // New with dotted access. @@ -170,7 +168,7 @@ void main() { testExpression("x << y + 1"); testExpression("x <<= y + 1"); // Array initializers. - testExpression('x = ["foo", "bar", x[4]]'); + testExpression("x = ['foo', 'bar', x[4]]"); testExpression("[]"); testError("[42 42]"); testExpression('beebop([1, 2, 3])'); diff --git a/pkg/compiler/test/js/size_estimator_expectations.json b/pkg/compiler/test/js/size_estimator_expectations.json index 2dacebb61fe..57b911ffbf0 100644 --- a/pkg/compiler/test/js/size_estimator_expectations.json +++ b/pkg/compiler/test/js/size_estimator_expectations.json @@ -102,8 +102,8 @@ }, { "original": "x = ['a', 'b', 'c']", - "expected": "#=[\"a\",\"b\",\"c\"]", - "minified": "x=[\"a\",\"b\",\"c\"]" + "expected": "#=['a','b','c']", + "minified": "x=['a','b','c']" }, { "original": "a = {'b': 1, 'c': 2}", @@ -154,8 +154,8 @@ }, { "original": "if (x == true) { return true; } else if (y < 3 || z > 5) { return l != null ? 'a' : 4; } else { foo(); return; }", - "expected": "if(#==!0)return !0;else if(#<3||#>5)return #!=null?\"a\":4;else{#();return;}", - "minified": "if(x==true)return true;else if(y<3||z>5)return l!=null?\"a\":4;else{foo();return}" + "expected": "if(#==!0)return !0;else if(#<3||#>5)return #!=null?'a':4;else{#();return;}", + "minified": "if(x==true)return true;else if(y<3||z>5)return l!=null?'a':4;else{foo();return}" }, { "original": "for (var a = 0; a < 10; a++) { foo(a); }", @@ -179,8 +179,8 @@ }, { "original": "switch (foo) { case 'a': case 'b': bar(); break; case 'c': 1; break; default: boo(); }", - "expected": "switch(#){case \"a\":case \"b\":#();break;case \"c\":1;break;default:#();}", - "minified": "switch(foo){case\"a\":case\"b\":bar();break;case\"c\":1;break;default:boo()}" + "expected": "switch(#){case 'a':case 'b':#();break;case 'c':1;break;default:#();}", + "minified": "switch(foo){case'a':case'b':bar();break;case'c':1;break;default:boo()}" }, { "original": "foo.prototype.Goo = function(a) { return a.bar(); }", @@ -193,4 +193,4 @@ "minified": "try{null=4}catch(e){print(e)}" } ] -} +} \ No newline at end of file diff --git a/pkg/js_ast/lib/js_ast.dart b/pkg/js_ast/lib/js_ast.dart index 682d91c7690..01b0dd9d9fb 100644 --- a/pkg/js_ast/lib/js_ast.dart +++ b/pkg/js_ast/lib/js_ast.dart @@ -7,7 +7,6 @@ library js_ast; import 'dart:collection' show IterableBase; import 'src/precedence.dart'; import 'src/characters.dart' as charCodes; -import 'src/strings.dart'; part 'src/nodes.dart'; part 'src/builder.dart'; diff --git a/pkg/js_ast/lib/src/builder.dart b/pkg/js_ast/lib/src/builder.dart index 1f980eaf9a0..2fee9468756 100644 --- a/pkg/js_ast/lib/src/builder.dart +++ b/pkg/js_ast/lib/src/builder.dart @@ -296,29 +296,186 @@ class JsBuilder { } /// Creates a literal js string from [value]. - LiteralString string(String value) => LiteralString(value); + LiteralString _legacyEscapedString(String value) { + // Start by escaping the backslashes. + String escaped = value.replaceAll('\\', '\\\\'); + // Do not escape unicode characters and ' because they are allowed in the + // string literal anyway. + escaped = escaped.replaceAllMapped(new RegExp('\n|"|\b|\t|\v|\r'), (match) { + switch (match.group(0)) { + case "\n": + return r"\n"; + case "\"": + return r'\"'; + case "\b": + return r"\b"; + case "\t": + return r"\t"; + case "\f": + return r"\f"; + case "\r": + return r"\r"; + case "\v": + return r"\v"; + } + throw new UnsupportedError("Unexpected match: ${match.group(0)}"); + }); + LiteralString result = string(escaped); + // We don't escape ' under the assumption that the string is wrapped + // into ". Verify that assumption. + assert(result.value.codeUnitAt(0) == '"'.codeUnitAt(0)); + return result; + } + + /// Creates a literal js string from [value]. + LiteralString escapedString(String value, + {bool utf8: false, bool ascii: false}) { + if (utf8 == false && ascii == false) return _legacyEscapedString(value); + if (utf8 && ascii) throw new ArgumentError('Cannot be both UTF8 and ASCII'); + + int singleQuotes = 0; + int doubleQuotes = 0; + int otherEscapes = 0; + int unpairedSurrogates = 0; + + for (int rune in value.runes) { + if (rune == charCodes.$BACKSLASH) { + ++otherEscapes; + } else if (rune == charCodes.$SQ) { + ++singleQuotes; + } else if (rune == charCodes.$DQ) { + ++doubleQuotes; + } else if (rune == charCodes.$LF || + rune == charCodes.$CR || + rune == charCodes.$LS || + rune == charCodes.$PS) { + // Line terminators. + ++otherEscapes; + } else if (rune == charCodes.$BS || + rune == charCodes.$TAB || + rune == charCodes.$VTAB || + rune == charCodes.$FF) { + ++otherEscapes; + } else if (_isUnpairedSurrogate(rune)) { + ++unpairedSurrogates; + } else { + if (ascii && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) { + ++otherEscapes; + } + } + } + + LiteralString finish(String quote, String contents) { + return new LiteralString('$quote$contents$quote'); + } + + if (otherEscapes == 0 && unpairedSurrogates == 0) { + if (doubleQuotes == 0) return finish('"', value); + if (singleQuotes == 0) return finish("'", value); + } + + bool useSingleQuotes = singleQuotes < doubleQuotes; + + StringBuffer sb = new StringBuffer(); + + for (int rune in value.runes) { + String escape = _irregularEscape(rune, useSingleQuotes); + if (escape != null) { + sb.write(escape); + continue; + } + if (rune == charCodes.$LS || + rune == charCodes.$PS || + _isUnpairedSurrogate(rune) || + ascii && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) { + if (rune < 0x100) { + sb.write(r'\x'); + sb.write(rune.toRadixString(16).padLeft(2, '0')); + } else if (rune < 0x10000) { + sb.write(r'\u'); + sb.write(rune.toRadixString(16).padLeft(4, '0')); + } else { + // Not all browsers accept the ES6 \u{zzzzzz} encoding, so emit two + // surrogate pairs. + var bits = rune - 0x10000; + var leading = 0xD800 | (bits >> 10); + var trailing = 0xDC00 | (bits & 0x3ff); + sb.write(r'\u'); + sb.write(leading.toRadixString(16)); + sb.write(r'\u'); + sb.write(trailing.toRadixString(16)); + } + } else { + sb.writeCharCode(rune); + } + } + + return finish(useSingleQuotes ? "'" : '"', sb.toString()); + } + + static bool _isUnpairedSurrogate(int code) => (code & 0xFFFFF800) == 0xD800; + + static String _irregularEscape(int code, bool useSingleQuotes) { + switch (code) { + case charCodes.$SQ: + return useSingleQuotes ? r"\'" : r"'"; + case charCodes.$DQ: + return useSingleQuotes ? r'"' : r'\"'; + case charCodes.$BACKSLASH: + return r'\\'; + case charCodes.$BS: + return r'\b'; + case charCodes.$TAB: + return r'\t'; + case charCodes.$LF: + return r'\n'; + case charCodes.$VTAB: + return r'\v'; + case charCodes.$FF: + return r'\f'; + case charCodes.$CR: + return r'\r'; + } + return null; + } + + /// Creates a literal js string from [value]. + /// + /// Note that this function only puts quotes around [value]. It does not do + /// any escaping, so use only when you can guarantee that [value] does not + /// contain newlines or backslashes. For escaping the string use + /// [escapedString]. + LiteralString string(String value) => new LiteralString('"$value"'); /// Creates an instance of [LiteralString] from [value]. /// /// Does not add quotes or do any escaping. LiteralString stringPart(String value) => new LiteralString(value); - StringConcatenation concatenateStrings(Iterable parts) { - return StringConcatenation(List.of(parts, growable: false)); - } - - Iterable joinLiterals( - Iterable items, Literal separator) sync* { - bool first = true; - for (final item in items) { - if (!first) yield separator; - yield item; - first = false; + StringConcatenation concatenateStrings(Iterable parts, + {addQuotes: false}) { + List _parts; + if (addQuotes) { + Literal quote = stringPart('"'); + _parts = [quote] + ..addAll(parts) + ..add(quote); + } else { + _parts = new List.from(parts, growable: false); } + return new StringConcatenation(_parts); } - LiteralString quoteName(Name name) { - return LiteralStringFromName(name); + Iterable joinLiterals(Iterable list, Literal separator) { + return new _InterleaveIterable(list, separator); + } + + LiteralString quoteName(Name name, {allowNull: false}) { + if (name == null) { + assert(allowNull); + return new LiteralString('""'); + } + return new LiteralStringFromName(name); } LiteralNumber number(num value) => new LiteralNumber('$value'); @@ -348,19 +505,18 @@ class JsBuilder { } LiteralString string(String value) => js.string(value); +LiteralString quoteName(Name name, {allowNull: false}) { + return js.quoteName(name, allowNull: allowNull); +} -/// Returns a LiteralString which has contents determined by [Name]. -/// -/// This is used to force a Name to be a string literal regardless of -/// context. It is not necessary for properties. -LiteralString quoteName(Name name) => js.quoteName(name); - +LiteralString stringPart(String value) => js.stringPart(value); Iterable joinLiterals(Iterable list, Literal separator) { return js.joinLiterals(list, separator); } -StringConcatenation concatenateStrings(Iterable parts) { - return js.concatenateStrings(parts); +StringConcatenation concatenateStrings(Iterable parts, + {addQuotes: false}) { + return js.concatenateStrings(parts, addQuotes: addQuotes); } LiteralNumber number(num value) => js.number(value); @@ -580,7 +736,7 @@ class MiniJsParser { '/': 5, '%': 5 }; - static final UNARY_OPERATORS = { + static final UNARY_OPERATORS = [ '++', '--', '+', @@ -591,7 +747,7 @@ class MiniJsParser { 'void', 'delete', 'await' - }; + ].toSet(); static final OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS = ['typeof', 'void', 'delete', 'in', 'instanceof', 'await'].toSet(); @@ -601,7 +757,7 @@ class MiniJsParser { return CATEGORIES[code]; } - String getRegExp(int startPosition) { + String getDelimited(int startPosition) { position = startPosition; int delimiter = src.codeUnitAt(startPosition); int currentCode; @@ -618,7 +774,7 @@ class MiniJsParser { escaped == charCodes.$u || escaped == charCodes.$U || category(escaped) == NUMERIC) { - error('Numeric and hex escapes are not supported in RegExp literals'); + error('Numeric and hex escapes are not allowed in literals'); } } } while (currentCode != delimiter); @@ -626,46 +782,6 @@ class MiniJsParser { return src.substring(lastPosition, position); } - String getString(int startPosition, int quote) { - assert(src.codeUnitAt(startPosition) == quote); - position = startPosition + 1; - final value = StringBuffer(); - while (true) { - if (position >= src.length) error("Unterminated literal"); - int code = src.codeUnitAt(position++); - if (code == quote) break; - if (code == charCodes.$LF) error("Unterminated literal"); - if (code == charCodes.$BACKSLASH) { - if (position >= src.length) error("Unterminated literal"); - code = src.codeUnitAt(position++); - if (code == charCodes.$f) { - value.writeCharCode(12); - } else if (code == charCodes.$n) { - value.writeCharCode(10); - } else if (code == charCodes.$r) { - value.writeCharCode(13); - } else if (code == charCodes.$t) { - value.writeCharCode(8); - } else if (code == charCodes.$BACKSLASH || - code == charCodes.$SQ || - code == charCodes.$DQ) { - value.writeCharCode(code); - } else if (code == charCodes.$x || code == charCodes.$X) { - error('Hex escapes not supported in string literals'); - } else if (code == charCodes.$u || code == charCodes.$U) { - error('Unicode escapes not supported in string literals'); - } else if (charCodes.$0 <= code && code <= charCodes.$9) { - error('Numeric escapes not supported in string literals'); - } else { - error('Unknown escape U+${code.toRadixString(16).padLeft(4, '0')}'); - } - continue; - } - value.writeCharCode(code); - } - return value.toString(); - } - void getToken() { skippedNewline = false; for (;;) { @@ -701,7 +817,7 @@ class MiniJsParser { if (code == charCodes.$SQ || code == charCodes.$DQ) { // String literal. lastCategory = STRING; - lastToken = getString(position, code); + lastToken = getDelimited(position); } else if (code == charCodes.$0 && position + 2 < src.length && src.codeUnitAt(position + 1) == charCodes.$x) { @@ -863,7 +979,7 @@ class MiniJsParser { } return new ArrayInitializer(values); } else if (last != null && last.startsWith("/")) { - String regexp = getRegExp(lastPosition); + String regexp = getDelimited(lastPosition); getToken(); String flags = lastToken; if (!acceptCategory(ALPHA)) flags = ""; @@ -937,12 +1053,12 @@ class MiniJsParser { Literal propertyName; String identifier = lastToken; if (acceptCategory(ALPHA)) { - propertyName = LiteralString(identifier); + propertyName = new LiteralString('"$identifier"'); } else if (acceptCategory(STRING)) { - propertyName = LiteralString(identifier); + propertyName = new LiteralString(identifier); } else if (acceptCategory(SYMBOL)) { // e.g. void - propertyName = LiteralString(identifier); + propertyName = new LiteralString('"$identifier"'); } else if (acceptCategory(HASH)) { var nameOrPosition = parseHash(); InterpolatedLiteral interpolatedLiteral = @@ -1458,3 +1574,40 @@ class MiniJsParser { return new Catch(errorName, body); } } + +class _InterleaveIterator implements Iterator { + Iterator source; + T separator; + bool isNextSeparator = false; + bool isInitialized = false; + + _InterleaveIterator(this.source, this.separator); + + bool moveNext() { + if (!isInitialized) { + isInitialized = true; + return source.moveNext(); + } else if (isNextSeparator) { + isNextSeparator = false; + return true; + } else { + return isNextSeparator = source.moveNext(); + } + } + + T get current { + if (isNextSeparator) return separator; + return source.current; + } +} + +class _InterleaveIterable extends IterableBase { + Iterable source; + T separator; + + _InterleaveIterable(this.source, this.separator); + + Iterator get iterator { + return new _InterleaveIterator(source.iterator, separator); + } +} diff --git a/pkg/js_ast/lib/src/nodes.dart b/pkg/js_ast/lib/src/nodes.dart index 285e9180743..903026edd6d 100644 --- a/pkg/js_ast/lib/src/nodes.dart +++ b/pkg/js_ast/lib/src/nodes.dart @@ -1038,8 +1038,7 @@ class LiteralStringFromName extends LiteralString { @override bool get isFinalized => name.isFinalized; - @override - String get value => name.name; + String get value => '"${name.name}"'; void visitChildren(NodeVisitor visitor) { name.accept(visitor); @@ -1372,8 +1371,6 @@ class Postfix extends Expression { int get precedenceLevel => UNARY; } -RegExp _identifierRE = new RegExp(r'^[A-Za-z_$][A-Za-z_$0-9]*$'); - abstract class VariableReference extends Expression { final String name; @@ -1381,6 +1378,8 @@ abstract class VariableReference extends Expression { assert(_identifierRE.hasMatch(name), "Non-identifier name '$name'"); } + static RegExp _identifierRE = new RegExp(r'^[A-Za-z_$][A-Za-z_$0-9]*$'); + T accept(NodeVisitor visitor); int get precedenceLevel => PRIMARY; @@ -1521,10 +1520,10 @@ class PropertyAccess extends Expression { PropertyAccess(this.receiver, this.selector); PropertyAccess.field(this.receiver, String fieldName) - : selector = LiteralString(fieldName); + : selector = new LiteralString('"$fieldName"'); PropertyAccess.indexed(this.receiver, int index) - : selector = LiteralNumber('$index'); + : selector = new LiteralNumber('$index'); T accept(NodeVisitor visitor) => visitor.visitAccess(this); @@ -1634,11 +1633,16 @@ class LiteralNull extends Literal { class LiteralString extends Literal { final String value; - /// Constructs a LiteralString for a string containing the characters of - /// `value`. - /// - /// When printed, the string will be escaped and quoted according to the - /// printer's settings. + /** + * Constructs a LiteralString from a string value. + * + * The constructor does not add the required quotes. If [value] is not + * surrounded by quotes and properly escaped, the resulting object is invalid + * as a JS value. + * + * TODO(sra): Introduce variants for known valid strings that don't allocate a + * new string just to add quotes. + */ LiteralString(this.value); T accept(NodeVisitor visitor) => visitor.visitLiteralString(this); @@ -1646,39 +1650,17 @@ class LiteralString extends Literal { R accept1(NodeVisitor1 visitor, A arg) => visitor.visitLiteralString(this, arg); - LiteralString _clone() => LiteralString(value); - - @override - String toString() { - final sb = StringBuffer('$runtimeType("'); - String end = '"'; - int count = 0; - for (int rune in value.runes) { - if (++count > 20) { - end = '"...'; - break; - } - if (32 <= rune && rune < 127) { - sb.writeCharCode(rune); - } else { - sb.write(r'\u{'); - sb.write(rune.toRadixString(16)); - sb.write(r'}'); - } - } - sb.write(end); - sb.write(')'); - return sb.toString(); - } + LiteralString _clone() => new LiteralString(value); } class StringConcatenation extends Literal { final List parts; - /// Constructs a StringConcatenation from a list of Literal elements. - /// - /// The constructor does not add surrounding quotes to the resulting - /// concatenated string. + /** + * Constructs a StringConcatenation from a list of Literal elements. + * The constructor does not add surrounding quotes to the resulting + * concatenated string. + */ StringConcatenation(this.parts); T accept(NodeVisitor visitor) => visitor.visitStringConcatenation(this); diff --git a/pkg/js_ast/lib/src/printer.dart b/pkg/js_ast/lib/src/printer.dart index ed165c25a3f..c6e9a9e79c9 100644 --- a/pkg/js_ast/lib/src/printer.dart +++ b/pkg/js_ast/lib/src/printer.dart @@ -5,16 +5,14 @@ part of js_ast; class JavaScriptPrintingOptions { - final bool utf8; final bool shouldCompressOutput; final bool minifyLocalVariables; final bool preferSemicolonToNewlineInMinifiedOutput; const JavaScriptPrintingOptions({ - this.utf8 = false, - this.shouldCompressOutput = false, - this.minifyLocalVariables = false, - this.preferSemicolonToNewlineInMinifiedOutput = false, + this.shouldCompressOutput: false, + this.minifyLocalVariables: false, + this.preferSemicolonToNewlineInMinifiedOutput: false, }); } @@ -58,10 +56,11 @@ class SimpleJavaScriptPrintingContext extends JavaScriptPrintingContext { String getText() => buffer.toString(); } -String DebugPrint(Node node, {bool utf8 = false}) { - JavaScriptPrintingOptions options = JavaScriptPrintingOptions(utf8: utf8); - SimpleJavaScriptPrintingContext context = SimpleJavaScriptPrintingContext(); - Printer printer = Printer(options, context); +String DebugPrint(Node node) { + JavaScriptPrintingOptions options = new JavaScriptPrintingOptions(); + SimpleJavaScriptPrintingContext context = + new SimpleJavaScriptPrintingContext(); + Printer printer = new Printer(options, context); printer.visit(node); return context.getText(); } @@ -84,8 +83,8 @@ class Printer implements NodeVisitor { // A cache of all indentation strings used so far. List _indentList = [""]; - static final identifierCharacterRegExp = RegExp(r'^[a-zA-Z_0-9$]'); - static final expressionContinuationRegExp = RegExp(r'^[-+([]'); + static final identifierCharacterRegExp = new RegExp(r'^[a-zA-Z_0-9$]'); + static final expressionContinuationRegExp = new RegExp(r'^[-+([]'); Printer(JavaScriptPrintingOptions options, JavaScriptPrintingContext context) : options = options, @@ -727,10 +726,10 @@ class Printer implements NodeVisitor { if (value is This) return true; if (value is LiteralNull) return true; if (value is LiteralNumber) return true; - if (value is LiteralString && value.value.length <= 6) return true; + if (value is LiteralString && value.value.length <= 8) return true; if (value is ObjectInitializer && value.properties.isEmpty) return true; if (value is ArrayInitializer && value.elements.isEmpty) return true; - if (value is Name && value.name.length <= 6) return true; + if (value is Name && value.name.length <= 8) return true; } return false; } @@ -1019,24 +1018,24 @@ class Printer implements NodeVisitor { } bool isValidJavaScriptId(String field) { - if (field.length == 0) return false; + if (field.length < 3) return false; // Ignore the leading and trailing string-delimiter. - for (int i = 0; i < field.length; i++) { + for (int i = 1; i < field.length - 1; i++) { // TODO(floitsch): allow more characters. int charCode = field.codeUnitAt(i); if (!(charCodes.$a <= charCode && charCode <= charCodes.$z || charCodes.$A <= charCode && charCode <= charCodes.$Z || charCode == charCodes.$$ || charCode == charCodes.$_ || - i > 0 && isDigit(charCode))) { + i != 1 && isDigit(charCode))) { return false; } } // TODO(floitsch): normally we should also check that the field is not a // reserved word. We don't generate fields with reserved word names except // for 'super'. - if (field == 'super') return false; - if (field == 'catch') return false; + if (field == '"super"') return false; + if (field == '"catch"') return false; return true; } @@ -1044,41 +1043,35 @@ class Printer implements NodeVisitor { void visitAccess(PropertyAccess access) { visitNestedExpression(access.receiver, CALL, newInForInit: inForInit, newAtStatementBegin: atStatementBegin); - Node selector = undefer(access.selector); if (selector is LiteralString) { - _dotString(access.selector, access.receiver, selector.value); - return; - } else if (selector is StringConcatenation) { - _dotString(access.selector, access.receiver, - _StringContentsCollector().collect(selector)); - return; + String fieldWithQuotes = selector.value; + if (isValidJavaScriptId(fieldWithQuotes)) { + if (access.receiver is LiteralNumber && + lastCharCode != charCodes.$CLOSE_PAREN) { + out(" ", isWhitespace: true); + } + out("."); + startNode(access.selector); + out(fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)); + endNode(access.selector); + return; + } } else if (selector is Name) { - _dotString(access.selector, access.receiver, selector.name); + Node receiver = undefer(access.receiver); + if (receiver is LiteralNumber && lastCharCode != charCodes.$CLOSE_PAREN) { + out(" ", isWhitespace: true); + } + out("."); + startNode(access.selector); + selector.accept(this); + endNode(access.selector); return; } - - out('['); + out("["); visitNestedExpression(access.selector, EXPRESSION, newInForInit: false, newAtStatementBegin: false); - out(']'); - } - - void _dotString(Node selector, Node receiver, String selectorValue) { - if (isValidJavaScriptId(selectorValue)) { - if (undefer(receiver) is LiteralNumber && - lastCharCode != charCodes.$CLOSE_PAREN) { - out(' ', isWhitespace: true); - } - out('.'); - startNode(selector); - out(selectorValue); - endNode(selector); - } else { - out('['); - _handleString(selectorValue); - out(']'); - } + out("]"); } @override @@ -1140,25 +1133,12 @@ class Printer implements NodeVisitor { @override void visitLiteralString(LiteralString node) { - _handleString(node.value); + out(node.value); } @override visitStringConcatenation(StringConcatenation node) { - _handleString(_StringContentsCollector().collect(node)); - } - - void _handleString(String value) { - final kind = StringToSource.analyze(value, utf8: options.utf8); - out(kind.quote); - if (kind.simple) { - out(value); - } else { - final sb = StringBuffer(); - StringToSource.writeString(sb, value, kind, utf8: options.utf8); - out(sb.toString()); - } - out(kind.quote); + node.visitChildren(this); } @override @@ -1255,15 +1235,18 @@ class Printer implements NodeVisitor { startNode(node.name); Node name = undefer(node.name); if (name is LiteralString) { - _outPropertyName(name.value); + String text = name.value; + if (isValidJavaScriptId(text)) { + out(text.substring(1, text.length - 1)); + } else { + out(text); + } } else if (name is Name) { - _outPropertyName(name.name); - } else if (name is LiteralNumber) { - out(name.value); + node.name.accept(this); } else { - // TODO(sra): Handle StringConcatenation. - // TODO(sra): Handle general expressions, .e.g. `{[x]: 1}`. - throw StateError('Unexpected Property name: $name'); + assert(name is LiteralNumber); + LiteralNumber nameNumber = node.name; + out(nameNumber.value); } endNode(node.name); out(":"); @@ -1272,14 +1255,6 @@ class Printer implements NodeVisitor { newInForInit: false, newAtStatementBegin: false); } - void _outPropertyName(String name) { - if (isValidJavaScriptId(name)) { - out(name); - } else { - _handleString(name); - } - } - @override void visitRegExpLiteral(RegExpLiteral node) { out(node.pattern); @@ -1346,44 +1321,6 @@ class Printer implements NodeVisitor { } } -class _StringContentsCollector extends BaseVisitor { - final StringBuffer _buffer = StringBuffer(); - - String collect(Node node) { - node.accept(this); - return _buffer.toString(); - } - - void _add(String value) { - _buffer.write(value); - } - - @override - void visitNode(Node node) { - throw StateError('Node should not be part of StringConcatenation: $node'); - } - - @override - void visitLiteralString(LiteralString node) { - _add(node.value); - } - - @override - void visitLiteralNumber(LiteralNumber node) { - _add(node.value); - } - - @override - void visitName(Name node) { - _add(node.name); - } - - @override - void visitStringConcatenation(StringConcatenation node) { - node.visitChildren(this); - } -} - class OrderedSet { final Set set; final List list; diff --git a/pkg/js_ast/lib/src/strings.dart b/pkg/js_ast/lib/src/strings.dart deleted file mode 100644 index 91c2bae6f5e..00000000000 --- a/pkg/js_ast/lib/src/strings.dart +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -// Utilities for converting between JavaScript source-code Strings and the -// String value they represent. - -import 'characters.dart' as charCodes; - -class StringToSourceKind { - /// [true] if preferable to use double quotes, [false] if preferable to use - /// single quotes. - final bool doubleQuotes; - - /// [true] if contents require no escaping with the preferred quoting. - final bool simple; - - const StringToSourceKind({this.doubleQuotes, this.simple}); - - String get quote => doubleQuotes ? '"' : "'"; -} - -class StringToSource { - const StringToSource(); - - static StringToSourceKind analyze(String value, {/*required*/ bool utf8}) { - final ascii = !utf8; - int singleQuotes = 0; - int doubleQuotes = 0; - int otherEscapes = 0; - int unpairedSurrogates = 0; - - for (int rune in value.runes) { - if (rune == charCodes.$BACKSLASH) { - ++otherEscapes; - } else if (rune == charCodes.$SQ) { - ++singleQuotes; - } else if (rune == charCodes.$DQ) { - ++doubleQuotes; - } else if (rune == charCodes.$LF || - rune == charCodes.$CR || - rune == charCodes.$LS || - rune == charCodes.$PS) { - // Line terminators. - ++otherEscapes; - } else if (rune == charCodes.$BS || - rune == charCodes.$TAB || - rune == charCodes.$VTAB || - rune == charCodes.$FF) { - ++otherEscapes; - } else if (ascii && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) { - ++otherEscapes; - } else if (_isUnpairedSurrogate(rune)) { - // Need to escape unpaired surrogates in a UTF8-encoded output otherwise - // the output would be malformed. - ++unpairedSurrogates; - } - } - - if (otherEscapes == 0 && unpairedSurrogates == 0) { - if (doubleQuotes == 0) { - return const StringToSourceKind(doubleQuotes: true, simple: true); - } - if (singleQuotes == 0) { - return const StringToSourceKind(doubleQuotes: false, simple: true); - } - } - - return doubleQuotes <= singleQuotes - ? const StringToSourceKind(doubleQuotes: true, simple: false) - : const StringToSourceKind(doubleQuotes: false, simple: false); - } - - static void writeString( - StringBuffer sb, String string, StringToSourceKind kind, - {/*required*/ bool utf8}) { - for (int rune in string.runes) { - String escape = _irregularEscape(rune, kind.doubleQuotes); - if (escape != null) { - sb.write(escape); - continue; - } - if (rune == charCodes.$LS || - rune == charCodes.$PS || - _isUnpairedSurrogate(rune) || - !utf8 && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) { - if (rune < 0x100) { - sb.write(r'\x'); - sb.write(rune.toRadixString(16).padLeft(2, '0')); - } else if (rune < 0x10000) { - sb.write(r'\u'); - sb.write(rune.toRadixString(16).padLeft(4, '0')); - } else { - // Not all browsers accept the ES6 \u{zzzzzz} encoding, so emit two - // surrogate pairs. - var bits = rune - 0x10000; - var leading = 0xD800 | (bits >> 10); - var trailing = 0xDC00 | (bits & 0x3ff); - sb.write(r'\u'); - sb.write(leading.toRadixString(16)); - sb.write(r'\u'); - sb.write(trailing.toRadixString(16)); - } - } else { - sb.writeCharCode(rune); - } - } - } - - static bool _isUnpairedSurrogate(int code) => (code & 0xFFFFF800) == 0xD800; - - static String _irregularEscape(int code, bool useDoubleQuotes) { - switch (code) { - case charCodes.$SQ: - return useDoubleQuotes ? r"'" : r"\'"; - case charCodes.$DQ: - return useDoubleQuotes ? r'\"' : r'"'; - case charCodes.$BACKSLASH: - return r'\\'; - case charCodes.$BS: - return r'\b'; - case charCodes.$TAB: - return r'\t'; - case charCodes.$LF: - return r'\n'; - case charCodes.$VTAB: - return r'\v'; - case charCodes.$FF: - return r'\f'; - case charCodes.$CR: - return r'\r'; - } - return null; - } -} diff --git a/pkg/js_ast/lib/src/template.dart b/pkg/js_ast/lib/src/template.dart index 788e081b097..5aa45ba879f 100644 --- a/pkg/js_ast/lib/src/template.dart +++ b/pkg/js_ast/lib/src/template.dart @@ -268,7 +268,7 @@ class InstantiatorGeneratorVisitor implements NodeVisitor { return (arguments) { var value = arguments[nameOrPosition]; if (value is Expression) return value; - if (value is String) return LiteralString(value); + if (value is String) return new LiteralString('"$value"'); throw error( 'Interpolated value #$nameOrPosition is not a selector: $value'); }; diff --git a/pkg/js_ast/test/deferred_expression_test.dart b/pkg/js_ast/test/deferred_expression_test.dart index afd9805573e..c91a151034a 100644 --- a/pkg/js_ast/test/deferred_expression_test.dart +++ b/pkg/js_ast/test/deferred_expression_test.dart @@ -7,15 +7,15 @@ import 'package:js_ast/js_ast.dart'; main() { Map map = {}; - VariableUse variableUse = VariableUse('variable'); + VariableUse variableUse = new VariableUse('variable'); DeferredExpression deferred = - map[variableUse] = _DeferredExpression(variableUse); - VariableUse variableUseAlias = VariableUse('variable'); - map[variableUseAlias] = _DeferredExpression(variableUseAlias); + map[variableUse] = new _DeferredExpression(variableUse); + VariableUse variableUseAlias = new VariableUse('variable'); + map[variableUseAlias] = new _DeferredExpression(variableUseAlias); - map[deferred] = _DeferredExpression(deferred); - Literal literal = LiteralString('literal'); - map[literal] = _DeferredExpression(literal); + map[deferred] = new _DeferredExpression(deferred); + Literal literal = new LiteralString('"literal"'); + map[literal] = new _DeferredExpression(literal); test(map, '#', [variableUse], 'variable'); test(map, '#', [deferred], 'variable'); @@ -54,18 +54,18 @@ void test(Map map, String template, List arguments, String expectedOutput) { Expression directExpression = js.expressionTemplateFor(template).instantiate(arguments); - _Context directContext = _Context(); + _Context directContext = new _Context(); Printer directPrinter = - Printer(const JavaScriptPrintingOptions(), directContext); + new Printer(const JavaScriptPrintingOptions(), directContext); directPrinter.visit(directExpression); Expect.equals(expectedOutput, directContext.text); Expression deferredExpression = js .expressionTemplateFor(template) .instantiate(arguments.map((e) => map[e]).toList()); - _Context deferredContext = _Context(); + _Context deferredContext = new _Context(); Printer deferredPrinter = - Printer(const JavaScriptPrintingOptions(), deferredContext); + new Printer(const JavaScriptPrintingOptions(), deferredContext); deferredPrinter.visit(deferredExpression); Expect.equals(expectedOutput, deferredContext.text); @@ -121,7 +121,7 @@ class _DeferredExpression extends DeferredExpression { } class _Context implements JavaScriptPrintingContext { - StringBuffer sb = StringBuffer(); + StringBuffer sb = new StringBuffer(); List errors = []; Map enterPositions = {}; Map exitPositions = {}; @@ -140,7 +140,7 @@ class _Context implements JavaScriptPrintingContext { void exitNode( Node node, int startPosition, int endPosition, int closingPosition) { exitPositions[node] = - _Position(startPosition, endPosition, closingPosition); + new _Position(startPosition, endPosition, closingPosition); Expect.equals(enterPositions[node], startPosition); } diff --git a/pkg/js_ast/test/string_escape_test.dart b/pkg/js_ast/test/string_escape_test.dart index 78bc7583db9..9277ed658c3 100644 --- a/pkg/js_ast/test/string_escape_test.dart +++ b/pkg/js_ast/test/string_escape_test.dart @@ -12,9 +12,9 @@ const int $LCURLY = $OPEN_CURLY_BRACKET; const int $RCURLY = $CLOSE_CURLY_BRACKET; void main() { - check(input, expected, {bool utf8 = false}) { - if (input is List) input = String.fromCharCodes(input); - String actual = DebugPrint(js.string(input), utf8: utf8); + check(input, expected, {ascii: false, utf8: false}) { + if (input is List) input = new String.fromCharCodes(input); + String actual = js.escapedString(input, ascii: ascii, utf8: utf8).value; if (expected is List) { expect(actual.codeUnits, expected); } else { @@ -29,57 +29,79 @@ void main() { test('simple-escapes', () { check([$BS], [$DQ, $BACKSLASH, $b, $DQ]); + check([$BS], [$DQ, $BACKSLASH, $b, $DQ], ascii: true); check([$BS], [$DQ, $BACKSLASH, $b, $DQ], utf8: true); check([$LF], [$DQ, $BACKSLASH, $n, $DQ]); + check([$LF], [$DQ, $BACKSLASH, $n, $DQ], ascii: true); check([$LF], [$DQ, $BACKSLASH, $n, $DQ], utf8: true); - check([$FF], [$DQ, $BACKSLASH, $f, $DQ]); + check([$FF], [$DQ, $FF, $DQ]); + check([$FF], [$DQ, $BACKSLASH, $f, $DQ], ascii: true); check([$FF], [$DQ, $BACKSLASH, $f, $DQ], utf8: true); check([$CR], [$DQ, $BACKSLASH, $r, $DQ]); + check([$CR], [$DQ, $BACKSLASH, $r, $DQ], ascii: true); check([$CR], [$DQ, $BACKSLASH, $r, $DQ], utf8: true); check([$TAB], [$DQ, $BACKSLASH, $t, $DQ]); + check([$TAB], [$DQ, $BACKSLASH, $t, $DQ], ascii: true); check([$TAB], [$DQ, $BACKSLASH, $t, $DQ], utf8: true); check([$VTAB], [$DQ, $BACKSLASH, $v, $DQ]); + check([$VTAB], [$DQ, $BACKSLASH, $v, $DQ], ascii: true); check([$VTAB], [$DQ, $BACKSLASH, $v, $DQ], utf8: true); }); test('unnamed-control-codes-escapes', () { - check([0, 1, 2, 3], r'''"\x00\x01\x02\x03"'''); + check([0, 1, 2, 3], [$DQ, 0, 1, 2, 3, $DQ]); + check([0, 1, 2, 3], r'''"\x00\x01\x02\x03"''', ascii: true); check([0, 1, 2, 3], [$DQ, 0, 1, 2, 3, $DQ], utf8: true); }); test('line-separator', () { - check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ]); + // Legacy escaper is broken. + // check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ]); + check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ], ascii: true); check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ], utf8: true); }); test('page-separator', () { - check([$PS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $9, $DQ]); + // Legacy escaper is broken. + // check([$PS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $9, $DQ]); + check([$PS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $9, $DQ], ascii: true); check([$PS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $9, $DQ], utf8: true); }); + test('legacy-escaper-is-broken', () { + check([$LS], [$DQ, 0x2028, $DQ]); + check([$PS], [$DQ, 0x2029, $DQ]); + }); + test('choose-quotes', () { - check('"', [$SQ, $DQ, $SQ]); - check("'", [$DQ, $SQ, $DQ]); + check('\'', [$DQ, $SQ, $DQ]); + check('"', [$SQ, $DQ, $SQ], ascii: true); + check("'", [$DQ, $SQ, $DQ], ascii: true); + // Legacy always double-quotes + check([$DQ, $DQ, $SQ], [$DQ, $BACKSLASH, $DQ, $BACKSLASH, $DQ, $SQ, $DQ]); // Using single quotes saves us one backslash: - check([$DQ, $DQ, $SQ], [$SQ, $DQ, $DQ, $BACKSLASH, $SQ, $SQ]); - check([$DQ, $SQ, $SQ], [$DQ, $BACKSLASH, $DQ, $SQ, $SQ, $DQ]); + check([$DQ, $DQ, $SQ], [$SQ, $DQ, $DQ, $BACKSLASH, $SQ, $SQ], ascii: true); + check([$DQ, $SQ, $SQ], [$DQ, $BACKSLASH, $DQ, $SQ, $SQ, $DQ], ascii: true); }); test('u1234', () { - check('\u1234', [$DQ, $BACKSLASH, $u, $1, $2, $3, $4, $DQ]); + check('\u1234', [$DQ, 0x1234, $DQ]); + check('\u1234', [$DQ, $BACKSLASH, $u, $1, $2, $3, $4, $DQ], ascii: true); check('\u1234', [$DQ, 0x1234, $DQ], utf8: true); }); test('u12345', () { + check([0x12345], [$DQ, 55304, 57157, $DQ]); // TODO: ES6 option: //check([0x12345], - // [$DQ, $BACKSLASH, $u, $LCURLY, $1, $2, $3, $4, $5, $RCURLY, $DQ]); - check([0x12345], r'''"\ud808\udf45"'''); + // [$DQ, $BACKSLASH, $u, $LCURLY, $1, $2, $3, $4, $5, $RCURLY, $DQ], + // ascii: true); + check([0x12345], r'''"\ud808\udf45"''', ascii: true); check([ 0x12345 ], [ @@ -97,7 +119,7 @@ void main() { $4, $5, $DQ - ]); + ], ascii: true); check([0x12345], [$DQ, 55304, 57157, $DQ], utf8: true); }); @@ -105,16 +127,21 @@ void main() { // (0xD834, 0xDD1E) = 0x1D11E // Strings containing unpaired surrogates must be encoded to prevent // problems with the utf8 file-level encoding. - check([0xD834], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $DQ]); + check([0xD834], [$DQ, 0xD834, $DQ]); // Legacy escapedString broken. + check([0xD834], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $DQ], ascii: true); check([0xD834], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $DQ], utf8: true); - check([0xDD1E], [$DQ, $BACKSLASH, $u, $d, $d, $1, $e, $DQ]); + check([0xDD1E], [$DQ, 0xDD1E, $DQ]); // Legacy escapedString broken. + check([0xDD1E], [$DQ, $BACKSLASH, $u, $d, $d, $1, $e, $DQ], ascii: true); check([0xDD1E], [$DQ, $BACKSLASH, $u, $d, $d, $1, $e, $DQ], utf8: true); - check([0xD834, $A], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $A, $DQ]); + check([0xD834, $A], [$DQ, 0xD834, $A, $DQ]); // Legacy escapedString broken. + check([0xD834, $A], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $A, $DQ], + ascii: true); check([0xD834, $A], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $A, $DQ], utf8: true); + check([0xD834, 0xDD1E], [$DQ, 0xD834, 0xDD1E, $DQ]); // Legacy ok. check([ 0xD834, 0xDD1E @@ -133,8 +160,8 @@ void main() { $1, $e, $DQ - ]); - check([0xD834, 0xDD1E], r'''"\ud834\udd1e"'''); + ], ascii: true); + check([0xD834, 0xDD1E], r'''"\ud834\udd1e"''', ascii: true); check([0xD834, 0xDD1E], [$DQ, 0xD834, 0xDD1E, $DQ], utf8: true); }); }