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
5 changes: 4 additions & 1 deletion pkgs/code_builder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## 4.11.2-wip
## 4.12.0-wip

* Allow single quotes in strings passed to `literalString(raw:true)`. This
argument no longer guarantees a raw string is used, but results will have the
same behavior.
* Correct type annotations on nullable and generic variables created with
`declareVar`, `declareFinal`, and `declareConst`.

Expand Down
87 changes: 79 additions & 8 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,90 @@ Expression literalNum(num value) => LiteralExpression._('$value');

/// Create a literal expression from a string [value].
///
/// **NOTE**: The string is always formatted `'<value>'`.
/// Returns an expression for a string formatted `'<value>'`.
///
/// 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.
/// If [raw] is `true` returns an expression that will evaluate to a String
/// containing exactly the same content as [value]. The literal may use single
/// or double quotes, and may not actually be marked raw, depending on the
/// content. All disallowed characters are automatically escaped.
Expression literalString(String value, {bool raw = false}) {
if (raw && value.contains('\'')) {
throw ArgumentError('Cannot include a single quote in a raw string');
}
if (raw) return LiteralExpression._(_escapeString(value));
final escaped = value.replaceAll('\'', '\\\'').replaceAll('\n', '\\n');
return LiteralExpression._("${raw ? 'r' : ''}'$escaped'");
return LiteralExpression._("'$escaped'");
}

String _escapeString(String value) {
final original = value;
var hasSingleQuote = false;
var hasDoubleQuote = false;
var hasDollar = false;
var hasBackslash = false;
var canBeRaw = true;

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

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

if (canBeRaw && (hasDollar || hasBackslash)) {
if (!hasSingleQuote) return "r'$original'";
if (!hasDoubleQuote) return 'r"$original"';
}

if (!hasDollar) {
if (!hasSingleQuote) return "'$value'";
if (!hasDoubleQuote) return '"$value"';
}

value = value.replaceAll(_dollarQuoteRegexp, r'\');
return "'$value'";
}

/// 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';
}
Comment on lines +99 to +105
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The _hexLiteral function currently uses \x and pads to 2 digits, which is only valid for 8-bit characters (up to \xFF). While the current _escapeRegExp only matches characters within this range, this implementation is fragile if the regex is expanded in the future to include non-ASCII characters. Consider using the more robust \u{...} format which supports all Unicode code points in Dart.

String _hexLiteral(String input) {
  final value = input.runes.single
      .toRadixString(16)
      .toUpperCase();
  return '\\u{$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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The entry for the backslash character in _escapeMap is redundant because backslashes are explicitly handled in the replaceAllMapped callback within _escapeString (lines 75-78). Removing this entry would improve maintainability and avoid confusion about how backslashes are processed.

};

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

// _escapeMap.keys.map(_hexLiteral).join();
const _escapeMapRegexp = r'\x08\x09\x0A\x0B\x0C\x0D\x7F\x5C';

/// 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
2 changes: 1 addition & 1 deletion pkgs/code_builder/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: code_builder
version: 4.11.2-wip
version: 4.12.0-wip
description: A fluent, builder-based library for generating valid Dart code.
repository: https://github.com/dart-lang/tools/tree/main/pkgs/code_builder
issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Acode_builder
Expand Down
109 changes: 96 additions & 13 deletions pkgs/code_builder/test/specs/code/expression_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,107 @@ void main() {
});
});

test('should emit a String', () {
expect(literalString(r'$monkey'), equalsDart(r"'$monkey'"));
});
group('literalString legacy', () {
test('should emit a String', () {
expect(literalString(r'$monkey'), equalsDart(r"'$monkey'"));
});

test('should emit a raw String', () {
expect(literalString(r'$monkey', raw: true), equalsDart(r"r'$monkey'"));
});
test('should emit a raw String', () {
expect(literalString(r'$monkey', raw: true), equalsDart(r"r'$monkey'"));
});

test('should escape single quotes in a String', () {
expect(literalString(r"don't"), equalsDart(r"'don\'t'"));
});
test('should escape single quotes in a String', () {
expect(literalString(r"don't"), equalsDart(r"'don\'t'"));
});

test('does not allow single quote in raw string', () {
expect(() => literalString(r"don't", raw: true), throwsArgumentError);
test('should escape a newline in a string', () {
expect(literalString('some\nthing'), equalsDart(r"'some\nthing'"));
});
});

test('should escape a newline in a string', () {
expect(literalString('some\nthing'), equalsDart(r"'some\nthing'"));
group('literalString raw', () {
test('should emit a simple string', () {
expect(literalString(raw: true, 'foo'), equalsDart(r"'foo'"));
});

test('should emit an empty string', () {
expect(literalString(raw: true, ''), equalsDart("''"));
});

test('should use double quotes for just a single quote', () {
expect(literalString(raw: true, "'"), equalsDart('"\'"'));
});

test('should use single quotes for just a double quote', () {
expect(literalString(raw: true, '"'), equalsDart("'\"'"));
});

test('should use raw string for a single backslash', () {
expect(literalString(raw: true, '\\'), equalsDart("r'\\'"));
});

test('should emit unicode characters', () {
expect(literalString(raw: true, '😊'), equalsDart("'😊'"));
});

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

test('should use raw string for backslashes', () {
expect(literalString(raw: true, r'a\tb'), equalsDart("r'a\\tb'"));
});

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

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

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

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

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

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

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

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

test('should emit a && expression', () {
Expand Down
Loading