Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkgs/code_builder/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* Correct type annotations on nullable and generic variables created with
`declareVar`, `declareFinal`, and `declareConst`.
* Escape carriage return characters in `literalString`.

## 4.11.1

Expand Down
97 changes: 91 additions & 6 deletions pkgs/code_builder/lib/src/specs/expression/literal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,104 @@ Expression literalNum(num value) => LiteralExpression._('$value');

/// Create a literal expression from a string [value].
///
/// **NOTE**: The string is always formatted `'<value>'`.
/// The _content_ of [value] is used as the _source_ of the generated Dart
/// string wrapped in single quotes. For example, `literalString('\$foo')` will
/// generate `'$foo'`, which will be an interpolation in the generated source.
///
/// If [raw] is `true`, creates a raw String formatted `r'<value>'` and the
/// value may not contain a single quote.
/// Escapes single quotes and newlines in the value.
Expression literalString(String value, {bool raw = false}) {
/// If the content of [value] is intended to match the content of the generated
/// literal use the [fullEscape] argument to create a String expression with
/// whatever escaping is necessary to result in the same content. This may
/// result in a raw string.
///
/// To force a raw String use the [raw] argument to create a string formatted
/// `r'<value>'`. For example `literalString('\$foo', raw: true)` will generate
/// `r'$foo'` which includes a `$` character. Most callers will prefer
/// [fullEscape].
///
/// When [raw] is `true`, the value may not contain any single quotes.
/// When [raw] and [fullEscape] are `false`, single quotes are escaped.
///
/// Newlines and carriage returns are always escaped outside of triple quoted
/// strings to avoid invalid syntax.
Expression literalString(
String value, {
bool raw = false,
bool fullEscape = false,
}) {
if (fullEscape) return LiteralExpression._(_escapeString(value));
if (raw && value.contains('\'')) {
throw ArgumentError('Cannot include a single quote in a raw string');
}
final escaped = value.replaceAll('\'', '\\\'').replaceAll('\n', '\\n');
final escaped = value
.replaceAll('\'', '\\\'')
.replaceAll('\n', '\\n')
.replaceAll('\r', '\\r');
return LiteralExpression._("${raw ? 'r' : ''}'$escaped'");
}

String _escapeString(String value) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks familiar 😁

var hasSingleQuote = false;
var hasDoubleQuote = false;
var hasDollar = false;
var canBeRaw = true;

value = value.replaceAllMapped(_escapeRegExp, (match) {
final value = match[0]!;
if (value == "'") {
hasSingleQuote = true;
return value;
} else if (value == '"') {
hasDoubleQuote = true;
return value;
} else if (value == r'$') {
hasDollar = true;
return value;
}

canBeRaw = false;
return _escapeMap[value] ?? _hexLiteral(value);
});

if (!hasDollar) {
if (!hasSingleQuote) return "'$value'";
if (!hasDoubleQuote) return '"$value"';
} else if (canBeRaw) {
if (!hasSingleQuote) return "r'$value'";
if (!hasDoubleQuote) return 'r"$value"';
}
value = value.replaceAll(_dollarQuoteRegexp, r'\');
return "'$value'";
}

final _dollarQuoteRegexp = RegExp(r"(?=[$'])");

/// A map from whitespace characters & `\` to their escape sequences.
const _escapeMap = {
'\b': r'\b', // 08 - backspace
'\t': r'\t', // 09 - tab
'\n': r'\n', // 0A - new line
'\v': r'\v', // 0B - vertical tab
'\f': r'\f', // 0C - form feed
'\r': r'\r', // 0D - carriage return
'\x7F': r'\x7F', // delete
r'\': r'\\', // backslash
};

/// Given single-character string, return the hex-escaped equivalent.
String _hexLiteral(String input) {
final value = input.runes.single
.toRadixString(16)
.toUpperCase()
.padLeft(2, '0');
return '\\x$value';
}

/// A [RegExp] that matches whitespace characters that must be escaped and
/// single-quote, double-quote, and `$`
final _escapeRegExp = RegExp('[\$\'"\\x00-\\x07\\x0E-\\x1F$_escapeMapRegexp]');

final _escapeMapRegexp = _escapeMap.keys.map(_hexLiteral).join();

/// Create a literal `...` operator for use when creating a Map literal.
///
/// *NOTE* This is used as a sentinel when constructing a `literalMap` or a
Expand Down
68 changes: 68 additions & 0 deletions pkgs/code_builder/test/specs/code/expression_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,74 @@ void main() {
expect(literalString('some\nthing'), equalsDart(r"'some\nthing'"));
});

test('should escape a carriage return in a string', () {
expect(literalString('some\rthing'), equalsDart(r"'some\rthing'"));
});

test('avoids extra escapes on \\', () {
expect(literalString(r'a\tb'), equalsDart(r"'a\tb'"));
});

group('literalString with fullEscape', () {
test('should emit a simple string', () {
expect(literalString('foo', fullEscape: true), equalsDart(r"'foo'"));
});

test('should use double quotes if it contains single quotes', () {
expect(literalString("don't", fullEscape: true), equalsDart('"don\'t"'));
});

test('should use single quotes if it contains double quotes', () {
expect(
literalString('foo "bar"', fullEscape: true),
equalsDart('\'foo "bar"\''),
);
});

test('should escape single quotes if it contains both quotes', () {
expect(
literalString('don\'t "bar"', fullEscape: true),
equalsDart('\'don\\\'t "bar"\''),
);
});

test('should use raw single quotes for dollar signs if possible', () {
expect(
literalString(r'$foo', fullEscape: true),
equalsDart('r\'\$foo\''),
);
});

test('should use raw double quotes for dollar signs and single quotes '
'if possible', () {
expect(
literalString(r"don't $foo", fullEscape: true),
equalsDart('r"don\'t \$foo"'),
);
});

test('should escape if it contains dollar, single, and double quotes', () {
expect(
literalString('don\'t "bar" \$foo', fullEscape: true),
equalsDart('\'don\\\'t "bar" \\\$foo\''),
);
});

test('should escape control characters', () {
expect(
literalString('foo\nbar', fullEscape: true),
equalsDart('\'foo\\nbar\''),
);
});

test('should escape control characters and dollar signs', () {
expect(
literalString('foo\n\$bar', fullEscape: true),
equalsDart('\'foo\\n\\\$bar\''),
);
});
});

test('should emit a && expression', () {
expect(literalTrue.and(literalFalse), equalsDart('true && false'));
});
Expand Down
Loading