[dart2js/js_ast] Escape strings in the printer

The old way was for the JavaScript string literals to be pre-escaped.
This was cumbersome since each place that creates a JavaScript string
literal needs to make a decision about whether escaping is needed and
the kind of escaping (UTF8 or ASCII).

This change moves the responsibility for escaping into the js_ast printer.
The escaped text exists only while printing, which reduces the heap in a
large modular compile link scenario by 220MB, since the unescaped string
is in memory anyway.

There were three kinds of escaping - ASCII, UTF8, and 'legacy', if not
specified. This has been reduced to ASCII (default) and UTF8, chosen by
a printer option.


Change-Id: Ic57d8fb70a213d3518244f1a152cd33e54103259
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/200400
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Mayank Patke <fishythefish@google.com>
Reviewed-by: Joshua Litt <joshualitt@google.com>
This commit is contained in:
Stephen Adams 2021-05-19 21:32:52 +00:00 committed by commit-bot@chromium.org
parent 626b48090a
commit dc92f126e2
17 changed files with 467 additions and 416 deletions

View file

@ -157,7 +157,7 @@ class UnparsedNode extends DeferredString implements AstContainer {
}
}
}
_cachedLiteral = js.escapedString(text);
_cachedLiteral = js.string(text);
}
return _cachedLiteral;
}

View file

@ -749,24 +749,24 @@ class SizeEstimator implements NodeVisitor {
}
bool isValidJavaScriptId(String field) {
if (field.length < 3) return false;
if (field.length == 0) return false;
// Ignore the leading and trailing string-delimiter.
for (int i = 1; i < field.length - 1; i++) {
for (int i = 0; i < field.length; 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 != 1 && isDigit(charCode))) {
i > 0 && 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,16 +776,17 @@ class SizeEstimator implements NodeVisitor {
newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
Node selector = access.selector;
if (selector is LiteralString) {
String fieldWithQuotes = literalStringToString(selector);
if (isValidJavaScriptId(fieldWithQuotes)) {
String field = literalStringToString(selector);
if (isValidJavaScriptId(field)) {
if (access.receiver is LiteralNumber) {
// We can eliminate the space in some cases, but for simplicity we
// always assume it is necessary.
out(' '); // ' '
}
// '.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}'
out('.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}');
// '.${field}'
out('.');
out(field);
return;
}
} else if (selector is Name) {
@ -875,7 +876,9 @@ class SizeEstimator implements NodeVisitor {
@override
void visitLiteralString(LiteralString node) {
out('"');
out(literalStringToString(node));
out('"');
}
@override
@ -968,10 +971,12 @@ class SizeEstimator implements NodeVisitor {
if (name is LiteralString) {
String text = literalStringToString(name);
if (isValidJavaScriptId(text)) {
// '${text.substring(1, text.length - 1)}
out('${text.substring(1, text.length - 1)}');
out(text);
} else {
out(text); // '$text'
// Approximation to `_handleString(text)`.
out('"');
out(text);
out('"');
}
} else if (name is Name) {
node.name.accept(this);

View file

@ -148,7 +148,7 @@ class ModularConstantEmitter
jsAst.Expression visitString(StringConstantValue constant, [_]) {
String value = constant.stringValue;
if (value.length < StringReferencePolicy.minimumLength) {
return js.escapedString(value, ascii: true);
return js.string(value);
}
return StringReference(constant);
}
@ -288,8 +288,7 @@ class ConstantEmitter extends ModularConstantEmitter {
}
// Keys in literal maps must be emitted in place.
jsAst.Literal keyExpression =
js.escapedString(key.stringValue, ascii: true);
jsAst.Literal keyExpression = js.string(key.stringValue);
jsAst.Expression valueExpression =
_constantReferenceGenerator(constant.values[i]);
properties.add(new jsAst.Property(keyExpression, valueExpression));

View file

@ -109,9 +109,7 @@ class _RecipeGenerator implements DartTypeVisitor<void, void> {
return _finishEncoding(js.string(String.fromCharCodes(_codes)));
}
_flushCodes();
jsAst.LiteralString quote = jsAst.LiteralString('"');
return _finishEncoding(
jsAst.StringConcatenation([quote, ..._fragments, quote]));
return _finishEncoding(jsAst.StringConcatenation(_fragments));
}
void _start(TypeRecipe recipe) {
@ -487,13 +485,13 @@ class RulesetEncoder {
CommonElements get _commonElements => _dartTypes.commonElements;
ClassEntity get _objectClass => _commonElements.objectClass;
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("'");
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('"');
bool _isObject(InterfaceType type) => identical(type.element, _objectClass);
@ -522,28 +520,32 @@ 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<ClassEntity, ClassEntity> redirection) =>
js.concatenateStrings([
js.quoteName(_emitter.typeAccessNewRti(redirection.key)),
_doubleQuote,
_emitter.typeAccessNewRti(redirection.key),
_doubleQuote,
_colon,
js.quoteName(_emitter.typeAccessNewRti(redirection.value)),
_doubleQuote,
_emitter.typeAccessNewRti(redirection.value),
_doubleQuote,
]);
jsAst.StringConcatenation _encodeEntry(
MapEntry<InterfaceType, _RulesetEntry> entry) =>
js.concatenateStrings([
js.quoteName(_emitter.typeAccessNewRti(entry.key.element)),
_doubleQuote,
_emitter.typeAccessNewRti(entry.key.element),
_doubleQuote,
_colon,
_leftBrace,
...js.joinLiterals([
@ -558,7 +560,9 @@ class RulesetEncoder {
jsAst.StringConcatenation _encodeSupertype(
InterfaceType targetType, InterfaceType supertype) =>
js.concatenateStrings([
js.quoteName(_emitter.typeAccessNewRti(supertype.element)),
_doubleQuote,
_emitter.typeAccessNewRti(supertype.element),
_doubleQuote,
_colon,
_leftBracket,
...js.joinLiterals(
@ -571,30 +575,36 @@ class RulesetEncoder {
jsAst.StringConcatenation _encodeTypeVariable(InterfaceType targetType,
TypeVariableType typeVariable, DartType supertypeArgument) =>
js.concatenateStrings([
js.quoteName(_emitter.typeVariableAccessNewRti(typeVariable.element)),
_doubleQuote,
_emitter.typeVariableAccessNewRti(typeVariable.element),
_doubleQuote,
_colon,
_encodeSupertypeArgument(targetType, supertypeArgument),
]);
jsAst.Literal _encodeSupertypeArgument(
InterfaceType targetType, DartType supertypeArgument) =>
_recipeEncoder.encodeMetadataRecipe(
_emitter, targetType, supertypeArgument);
js.concatenateStrings([
_doubleQuote,
_recipeEncoder.encodeMetadataRecipe(
_emitter, targetType, supertypeArgument),
_doubleQuote
]);
jsAst.StringConcatenation encodeErasedTypes(
Map<ClassEntity, int> erasedTypes) =>
js.concatenateStrings([
_quote,
_leftBrace,
...js.joinLiterals(erasedTypes.entries.map(encodeErasedType), _comma),
_rightBrace,
_quote,
]);
jsAst.StringConcatenation encodeErasedType(
MapEntry<ClassEntity, int> entry) =>
js.concatenateStrings([
js.quoteName(_emitter.typeAccessNewRti(entry.key)),
_doubleQuote,
_emitter.typeAccessNewRti(entry.key),
_doubleQuote,
_colon,
js.number(entry.value),
]);
@ -602,20 +612,20 @@ class RulesetEncoder {
jsAst.StringConcatenation encodeTypeParameterVariances(
Map<ClassEntity, List<Variance>> typeParameterVariances) =>
js.concatenateStrings([
_quote,
_leftBrace,
...js.joinLiterals(
typeParameterVariances.entries
.map(_encodeTypeParameterVariancesForClass),
_comma),
_rightBrace,
_quote,
]);
jsAst.StringConcatenation _encodeTypeParameterVariancesForClass(
MapEntry<ClassEntity, List<Variance>> classEntry) =>
js.concatenateStrings([
js.quoteName(_emitter.typeAccessNewRti(classEntry.key)),
_doubleQuote,
_emitter.typeAccessNewRti(classEntry.key),
_doubleQuote,
_colon,
_leftBracket,
...js.joinLiterals(

View file

@ -256,8 +256,7 @@ class StringReferenceFinalizerImpl implements StringReferenceFinalizer {
for (_ReferenceSet referenceSet in _referencesByString.values) {
if (referenceSet.generateAtUse) {
StringConstantValue constant = referenceSet.constant;
js.Expression reference =
js.js.escapedString(constant.stringValue, ascii: true);
js.Expression reference = js.string(constant.stringValue);
for (StringReference ref in referenceSet._references) {
ref.value = reference;
}
@ -275,8 +274,7 @@ 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.js.escapedString(string, ascii: true)));
properties.add(js.Property(propertyName, js.string(string)));
var access = js.js('#.#', [holderLocalName, propertyName]);
for (StringReference ref in referenceSet._references) {
ref.value = access;

View file

@ -67,7 +67,7 @@ runTest(List<String> 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++;
}
});

View file

@ -101,7 +101,7 @@ class ModelIrComputer extends IrDataExtractor<Features> {
/// Call to fixed backend name, so we include the argument
/// values to test encoding of optional parameters in native
/// methods.
name = selector.value.substring(1, selector.value.length - 1);
name = selector.value;
fixedNameCall = true;
}
if (name != null) {
@ -146,7 +146,7 @@ class ModelIrComputer extends IrDataExtractor<Features> {
/// Call to fixed backend name, so we include the argument
/// values to test encoding of optional parameters in native
/// methods.
name = selector.value.substring(1, selector.value.length - 1);
name = selector.value;
}
if (receiverName != null && name != null) {
@ -236,7 +236,7 @@ class ModelIrComputer extends IrDataExtractor<Features> {
if (selector is js.Name) {
name = selector.key;
} else if (selector is js.LiteralString) {
name = selector.value.substring(1, selector.value.length - 1);
name = selector.value;
}
if (name != null) {
features.addElement(Tags.assignment, '${name}');

View file

@ -20,7 +20,9 @@ testExpression(String expression, [String expect = ""]) {
testError(String expression, [String expect = ""]) {
bool doCheck(exception) {
Expect.isTrue(exception.toString().contains(expect));
final exceptionText = '$exception';
Expect.isTrue(exceptionText.contains(expect),
'Missing "$expect" in "$exceptionText"');
return true;
}
@ -65,9 +67,9 @@ void main() {
// String literal with \n.
testExpression(r'var x = "\n"');
// String literal with escaped quote.
testExpression(r'var x = "\""');
testExpression(r'''var x = "\""''', r"""var x = '"'""");
// *No clever escapes.
testError(r'var x = "\x42"', 'escapes are not allowed in literals');
testError(r'var x = "\x42"', 'Hex escapes not supported');
// Operator new.
testExpression('new Foo()');
// New with dotted access.
@ -168,7 +170,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])');

View file

@ -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)}"
}
]
}
}

View file

@ -7,6 +7,7 @@ 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';

View file

@ -296,186 +296,29 @@ class JsBuilder {
}
/// Creates a literal js string from [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"');
LiteralString string(String value) => 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<Literal> parts,
{addQuotes: false}) {
List<Literal> _parts;
if (addQuotes) {
Literal quote = stringPart('"');
_parts = <Literal>[quote]
..addAll(parts)
..add(quote);
} else {
_parts = new List.from(parts, growable: false);
}
return new StringConcatenation(_parts);
StringConcatenation concatenateStrings(Iterable<Literal> parts) {
return StringConcatenation(List.of(parts, growable: false));
}
Iterable<Literal> joinLiterals(Iterable<Literal> list, Literal separator) {
return new _InterleaveIterable<Literal>(list, separator);
Iterable<Literal> joinLiterals(
Iterable<Literal> items, Literal separator) sync* {
bool first = true;
for (final item in items) {
if (!first) yield separator;
yield item;
first = false;
}
}
LiteralString quoteName(Name name, {allowNull: false}) {
if (name == null) {
assert(allowNull);
return new LiteralString('""');
}
return new LiteralStringFromName(name);
LiteralString quoteName(Name name) {
return LiteralStringFromName(name);
}
LiteralNumber number(num value) => new LiteralNumber('$value');
@ -505,18 +348,19 @@ class JsBuilder {
}
LiteralString string(String value) => js.string(value);
LiteralString quoteName(Name name, {allowNull: false}) {
return js.quoteName(name, allowNull: allowNull);
}
LiteralString stringPart(String value) => js.stringPart(value);
/// 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);
Iterable<Literal> joinLiterals(Iterable<Literal> list, Literal separator) {
return js.joinLiterals(list, separator);
}
StringConcatenation concatenateStrings(Iterable<Literal> parts,
{addQuotes: false}) {
return js.concatenateStrings(parts, addQuotes: addQuotes);
StringConcatenation concatenateStrings(Iterable<Literal> parts) {
return js.concatenateStrings(parts);
}
LiteralNumber number(num value) => js.number(value);
@ -736,7 +580,7 @@ class MiniJsParser {
'/': 5,
'%': 5
};
static final UNARY_OPERATORS = [
static final UNARY_OPERATORS = {
'++',
'--',
'+',
@ -747,7 +591,7 @@ class MiniJsParser {
'void',
'delete',
'await'
].toSet();
};
static final OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS =
['typeof', 'void', 'delete', 'in', 'instanceof', 'await'].toSet();
@ -757,7 +601,7 @@ class MiniJsParser {
return CATEGORIES[code];
}
String getDelimited(int startPosition) {
String getRegExp(int startPosition) {
position = startPosition;
int delimiter = src.codeUnitAt(startPosition);
int currentCode;
@ -774,7 +618,7 @@ class MiniJsParser {
escaped == charCodes.$u ||
escaped == charCodes.$U ||
category(escaped) == NUMERIC) {
error('Numeric and hex escapes are not allowed in literals');
error('Numeric and hex escapes are not supported in RegExp literals');
}
}
} while (currentCode != delimiter);
@ -782,6 +626,46 @@ 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 (;;) {
@ -817,7 +701,7 @@ class MiniJsParser {
if (code == charCodes.$SQ || code == charCodes.$DQ) {
// String literal.
lastCategory = STRING;
lastToken = getDelimited(position);
lastToken = getString(position, code);
} else if (code == charCodes.$0 &&
position + 2 < src.length &&
src.codeUnitAt(position + 1) == charCodes.$x) {
@ -979,7 +863,7 @@ class MiniJsParser {
}
return new ArrayInitializer(values);
} else if (last != null && last.startsWith("/")) {
String regexp = getDelimited(lastPosition);
String regexp = getRegExp(lastPosition);
getToken();
String flags = lastToken;
if (!acceptCategory(ALPHA)) flags = "";
@ -1053,12 +937,12 @@ class MiniJsParser {
Literal propertyName;
String identifier = lastToken;
if (acceptCategory(ALPHA)) {
propertyName = new LiteralString('"$identifier"');
propertyName = LiteralString(identifier);
} else if (acceptCategory(STRING)) {
propertyName = new LiteralString(identifier);
propertyName = LiteralString(identifier);
} else if (acceptCategory(SYMBOL)) {
// e.g. void
propertyName = new LiteralString('"$identifier"');
propertyName = LiteralString(identifier);
} else if (acceptCategory(HASH)) {
var nameOrPosition = parseHash();
InterpolatedLiteral interpolatedLiteral =
@ -1574,40 +1458,3 @@ class MiniJsParser {
return new Catch(errorName, body);
}
}
class _InterleaveIterator<T extends Node> implements Iterator<T> {
Iterator<T> 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<T extends Node> extends IterableBase<T> {
Iterable<T> source;
T separator;
_InterleaveIterable(this.source, this.separator);
Iterator<T> get iterator {
return new _InterleaveIterator<T>(source.iterator, separator);
}
}

View file

@ -1038,7 +1038,8 @@ class LiteralStringFromName extends LiteralString {
@override
bool get isFinalized => name.isFinalized;
String get value => '"${name.name}"';
@override
String get value => name.name;
void visitChildren<T>(NodeVisitor<T> visitor) {
name.accept(visitor);
@ -1371,6 +1372,8 @@ 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;
@ -1378,8 +1381,6 @@ 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<T>(NodeVisitor<T> visitor);
int get precedenceLevel => PRIMARY;
@ -1520,10 +1521,10 @@ class PropertyAccess extends Expression {
PropertyAccess(this.receiver, this.selector);
PropertyAccess.field(this.receiver, String fieldName)
: selector = new LiteralString('"$fieldName"');
: selector = LiteralString(fieldName);
PropertyAccess.indexed(this.receiver, int index)
: selector = new LiteralNumber('$index');
: selector = LiteralNumber('$index');
T accept<T>(NodeVisitor<T> visitor) => visitor.visitAccess(this);
@ -1633,16 +1634,11 @@ class LiteralNull extends Literal {
class LiteralString extends Literal {
final String value;
/**
* 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.
*/
/// 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.
LiteralString(this.value);
T accept<T>(NodeVisitor<T> visitor) => visitor.visitLiteralString(this);
@ -1650,17 +1646,39 @@ class LiteralString extends Literal {
R accept1<R, A>(NodeVisitor1<R, A> visitor, A arg) =>
visitor.visitLiteralString(this, arg);
LiteralString _clone() => new LiteralString(value);
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();
}
}
class StringConcatenation extends Literal {
final List<Literal> 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<T>(NodeVisitor<T> visitor) => visitor.visitStringConcatenation(this);

View file

@ -5,14 +5,16 @@
part of js_ast;
class JavaScriptPrintingOptions {
final bool utf8;
final bool shouldCompressOutput;
final bool minifyLocalVariables;
final bool preferSemicolonToNewlineInMinifiedOutput;
const JavaScriptPrintingOptions({
this.shouldCompressOutput: false,
this.minifyLocalVariables: false,
this.preferSemicolonToNewlineInMinifiedOutput: false,
this.utf8 = false,
this.shouldCompressOutput = false,
this.minifyLocalVariables = false,
this.preferSemicolonToNewlineInMinifiedOutput = false,
});
}
@ -56,11 +58,10 @@ class SimpleJavaScriptPrintingContext extends JavaScriptPrintingContext {
String getText() => buffer.toString();
}
String DebugPrint(Node node) {
JavaScriptPrintingOptions options = new JavaScriptPrintingOptions();
SimpleJavaScriptPrintingContext context =
new SimpleJavaScriptPrintingContext();
Printer printer = new Printer(options, context);
String DebugPrint(Node node, {bool utf8 = false}) {
JavaScriptPrintingOptions options = JavaScriptPrintingOptions(utf8: utf8);
SimpleJavaScriptPrintingContext context = SimpleJavaScriptPrintingContext();
Printer printer = Printer(options, context);
printer.visit(node);
return context.getText();
}
@ -83,8 +84,8 @@ class Printer implements NodeVisitor {
// A cache of all indentation strings used so far.
List<String> _indentList = <String>[""];
static final identifierCharacterRegExp = new RegExp(r'^[a-zA-Z_0-9$]');
static final expressionContinuationRegExp = new RegExp(r'^[-+([]');
static final identifierCharacterRegExp = RegExp(r'^[a-zA-Z_0-9$]');
static final expressionContinuationRegExp = RegExp(r'^[-+([]');
Printer(JavaScriptPrintingOptions options, JavaScriptPrintingContext context)
: options = options,
@ -726,10 +727,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 <= 8) return true;
if (value is LiteralString && value.value.length <= 6) 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 <= 8) return true;
if (value is Name && value.name.length <= 6) return true;
}
return false;
}
@ -1018,24 +1019,24 @@ class Printer implements NodeVisitor {
}
bool isValidJavaScriptId(String field) {
if (field.length < 3) return false;
if (field.length == 0) return false;
// Ignore the leading and trailing string-delimiter.
for (int i = 1; i < field.length - 1; i++) {
for (int i = 0; i < field.length; 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 != 1 && isDigit(charCode))) {
i > 0 && 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;
}
@ -1043,35 +1044,41 @@ 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) {
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;
}
_dotString(access.selector, access.receiver, selector.value);
return;
} else if (selector is StringConcatenation) {
_dotString(access.selector, access.receiver,
_StringContentsCollector().collect(selector));
return;
} else if (selector is 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);
_dotString(access.selector, access.receiver, selector.name);
return;
}
out("[");
out('[');
visitNestedExpression(access.selector, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out("]");
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(']');
}
}
@override
@ -1133,12 +1140,25 @@ class Printer implements NodeVisitor {
@override
void visitLiteralString(LiteralString node) {
out(node.value);
_handleString(node.value);
}
@override
visitStringConcatenation(StringConcatenation node) {
node.visitChildren(this);
_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);
}
@override
@ -1235,18 +1255,15 @@ class Printer implements NodeVisitor {
startNode(node.name);
Node name = undefer(node.name);
if (name is LiteralString) {
String text = name.value;
if (isValidJavaScriptId(text)) {
out(text.substring(1, text.length - 1));
} else {
out(text);
}
_outPropertyName(name.value);
} else if (name is Name) {
node.name.accept(this);
_outPropertyName(name.name);
} else if (name is LiteralNumber) {
out(name.value);
} else {
assert(name is LiteralNumber);
LiteralNumber nameNumber = node.name;
out(nameNumber.value);
// TODO(sra): Handle StringConcatenation.
// TODO(sra): Handle general expressions, .e.g. `{[x]: 1}`.
throw StateError('Unexpected Property name: $name');
}
endNode(node.name);
out(":");
@ -1255,6 +1272,14 @@ 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);
@ -1321,6 +1346,44 @@ class Printer implements NodeVisitor {
}
}
class _StringContentsCollector extends BaseVisitor<void> {
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<T> {
final Set<T> set;
final List<T> list;

View file

@ -0,0 +1,135 @@
// 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;
}
}

View file

@ -268,7 +268,7 @@ class InstantiatorGeneratorVisitor implements NodeVisitor<Instantiator> {
return (arguments) {
var value = arguments[nameOrPosition];
if (value is Expression) return value;
if (value is String) return new LiteralString('"$value"');
if (value is String) return LiteralString(value);
throw error(
'Interpolated value #$nameOrPosition is not a selector: $value');
};

View file

@ -7,15 +7,15 @@ import 'package:js_ast/js_ast.dart';
main() {
Map<Expression, DeferredExpression> map = {};
VariableUse variableUse = new VariableUse('variable');
VariableUse variableUse = VariableUse('variable');
DeferredExpression deferred =
map[variableUse] = new _DeferredExpression(variableUse);
VariableUse variableUseAlias = new VariableUse('variable');
map[variableUseAlias] = new _DeferredExpression(variableUseAlias);
map[variableUse] = _DeferredExpression(variableUse);
VariableUse variableUseAlias = VariableUse('variable');
map[variableUseAlias] = _DeferredExpression(variableUseAlias);
map[deferred] = new _DeferredExpression(deferred);
Literal literal = new LiteralString('"literal"');
map[literal] = new _DeferredExpression(literal);
map[deferred] = _DeferredExpression(deferred);
Literal literal = LiteralString('literal');
map[literal] = _DeferredExpression(literal);
test(map, '#', [variableUse], 'variable');
test(map, '#', [deferred], 'variable');
@ -54,18 +54,18 @@ void test(Map<Expression, DeferredExpression> map, String template,
List<Expression> arguments, String expectedOutput) {
Expression directExpression =
js.expressionTemplateFor(template).instantiate(arguments);
_Context directContext = new _Context();
_Context directContext = _Context();
Printer directPrinter =
new Printer(const JavaScriptPrintingOptions(), directContext);
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 = new _Context();
_Context deferredContext = _Context();
Printer deferredPrinter =
new Printer(const JavaScriptPrintingOptions(), deferredContext);
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 = new StringBuffer();
StringBuffer sb = StringBuffer();
List<String> errors = [];
Map<Node, int> enterPositions = {};
Map<Node, _Position> exitPositions = {};
@ -140,7 +140,7 @@ class _Context implements JavaScriptPrintingContext {
void exitNode(
Node node, int startPosition, int endPosition, int closingPosition) {
exitPositions[node] =
new _Position(startPosition, endPosition, closingPosition);
_Position(startPosition, endPosition, closingPosition);
Expect.equals(enterPositions[node], startPosition);
}

View file

@ -12,9 +12,9 @@ const int $LCURLY = $OPEN_CURLY_BRACKET;
const int $RCURLY = $CLOSE_CURLY_BRACKET;
void main() {
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;
check(input, expected, {bool utf8 = false}) {
if (input is List) input = String.fromCharCodes(input);
String actual = DebugPrint(js.string(input), utf8: utf8);
if (expected is List) {
expect(actual.codeUnits, expected);
} else {
@ -29,79 +29,57 @@ 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, $FF, $DQ]);
check([$FF], [$DQ, $BACKSLASH, $f, $DQ], ascii: true);
check([$FF], [$DQ, $BACKSLASH, $f, $DQ]);
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], [$DQ, 0, 1, 2, 3, $DQ]);
check([0, 1, 2, 3], r'''"\x00\x01\x02\x03"''', ascii: true);
check([0, 1, 2, 3], r'''"\x00\x01\x02\x03"''');
check([0, 1, 2, 3], [$DQ, 0, 1, 2, 3, $DQ], utf8: true);
});
test('line-separator', () {
// 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]);
check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ], utf8: true);
});
test('page-separator', () {
// 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]);
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('\'', [$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]);
check('"', [$SQ, $DQ, $SQ]);
check("'", [$DQ, $SQ, $DQ]);
// Using single quotes saves us one backslash:
check([$DQ, $DQ, $SQ], [$SQ, $DQ, $DQ, $BACKSLASH, $SQ, $SQ], ascii: true);
check([$DQ, $SQ, $SQ], [$DQ, $BACKSLASH, $DQ, $SQ, $SQ, $DQ], ascii: true);
check([$DQ, $DQ, $SQ], [$SQ, $DQ, $DQ, $BACKSLASH, $SQ, $SQ]);
check([$DQ, $SQ, $SQ], [$DQ, $BACKSLASH, $DQ, $SQ, $SQ, $DQ]);
});
test('u1234', () {
check('\u1234', [$DQ, 0x1234, $DQ]);
check('\u1234', [$DQ, $BACKSLASH, $u, $1, $2, $3, $4, $DQ], ascii: true);
check('\u1234', [$DQ, $BACKSLASH, $u, $1, $2, $3, $4, $DQ]);
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],
// ascii: true);
check([0x12345], r'''"\ud808\udf45"''', ascii: true);
// [$DQ, $BACKSLASH, $u, $LCURLY, $1, $2, $3, $4, $5, $RCURLY, $DQ]);
check([0x12345], r'''"\ud808\udf45"''');
check([
0x12345
], [
@ -119,7 +97,7 @@ void main() {
$4,
$5,
$DQ
], ascii: true);
]);
check([0x12345], [$DQ, 55304, 57157, $DQ], utf8: true);
});
@ -127,21 +105,16 @@ void main() {
// (0xD834, 0xDD1E) = 0x1D11E
// Strings containing unpaired surrogates must be encoded to prevent
// problems with the utf8 file-level encoding.
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]);
check([0xD834], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $DQ], utf8: true);
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]);
check([0xDD1E], [$DQ, $BACKSLASH, $u, $d, $d, $1, $e, $DQ], utf8: true);
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]);
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
@ -160,8 +133,8 @@ void main() {
$1,
$e,
$DQ
], ascii: true);
check([0xD834, 0xDD1E], r'''"\ud834\udd1e"''', ascii: true);
]);
check([0xD834, 0xDD1E], r'''"\ud834\udd1e"''');
check([0xD834, 0xDD1E], [$DQ, 0xD834, 0xDD1E, $DQ], utf8: true);
});
}