mirror of
https://github.com/dart-lang/sdk
synced 2024-09-15 22:51:29 +00:00
[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:
parent
626b48090a
commit
dc92f126e2
|
@ -157,7 +157,7 @@ class UnparsedNode extends DeferredString implements AstContainer {
|
|||
}
|
||||
}
|
||||
}
|
||||
_cachedLiteral = js.escapedString(text);
|
||||
_cachedLiteral = js.string(text);
|
||||
}
|
||||
return _cachedLiteral;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}');
|
||||
|
|
|
@ -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])');
|
||||
|
|
|
@ -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)}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
135
pkg/js_ast/lib/src/strings.dart
Normal file
135
pkg/js_ast/lib/src/strings.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue