Skip to content
Merged
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
6 changes: 6 additions & 0 deletions packages/ack_generator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
* `inference.dart` exports shared type and constraint inference for other
generators. The class-first generator uses this library.

### Fixed

* Escape line terminators and other control characters in every generated
string literal. `@Pattern`, discriminator, and key values containing a
newline or carriage return now produce valid Dart source.

## 1.6.2

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../json/helper_names.dart';
import '../models/schema_model_graph.dart';
import '../inference/schema_inference.dart';
import 'generated_companion_visibility.dart';
import '../utils/string_literal.dart';

typedef _ModelOptions = ({
String? schemaName,
Expand Down Expand Up @@ -614,7 +615,7 @@ final class ClassModelGraphBuilder {
isDiscriminator: isDiscriminator,
);
var schema = isDiscriminator && discriminatorValue != null
? '${_ack('Ack')}.literal(${_literal(discriminatorValue)})'
? '${_ack('Ack')}.literal(${dartStringLiteral(discriminatorValue)})'
: await _fieldSchema(field, futureType: futureType);
schema = _inference.applyDescription(schema, field);
schema = _applyPresence(
Expand Down Expand Up @@ -1940,14 +1941,6 @@ final class ClassModelGraphBuilder {
return prefix == null || prefix.isEmpty ? symbol : '$prefix.$symbol';
}

String _literal(String value) {
final escaped = value
.replaceAll(r'\', r'\\')
.replaceAll("'", r"\'")
.replaceAll(r'$', r'\$');
return "'$escaped'";
}

AckSchemaId _id(ClassElement element) => AckSchemaId(
libraryUri: library.element.uri,
declarationName: element.name!,
Expand Down
31 changes: 12 additions & 19 deletions packages/ack_generator/lib/src/builders/class_model_emitter.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '../json/helper_names.dart';
import '../models/schema_model_graph.dart';
import 'data_class_emitter.dart';
import '../utils/string_literal.dart';

/// Emits class-first schema codecs and top-level JSON/runtime glue.
final class AckClassModelEmitter {
Expand Down Expand Up @@ -106,10 +107,10 @@ final class AckClassModelEmitter {
for (final entry in node.branches.entries) {
final branch = nodes[entry.value]!;
schemaEntries.add(
'${_literal(entry.key)}: ${ackClassRawObjectName(branch.className)}',
'${dartStringLiteral(entry.key)}: ${ackClassRawObjectName(branch.className)}',
);
decodeCases.add(
'${_literal(entry.key)} => '
'${dartStringLiteral(entry.key)} => '
'${ackClassFromRuntimeName(branch.className)}(value)',
);
}
Expand Down Expand Up @@ -146,14 +147,14 @@ final class AckClassModelEmitter {
output
..writeln('''
final $rawName = ${_ack('Ack')}.discriminated(
discriminatorKey: ${_literal(node.discriminatorKey)},
discriminatorKey: ${dartStringLiteral(node.discriminatorKey)},
schemas: {${schemaEntries.join(', ')}},
);

final $wireName = ${_ack('Ack')}.preserveBoundary($rawName);

final ${metadata.backingName} = $rawName.codec<${node.className}>(
decode: (value) => switch (value[${_literal(node.discriminatorKey)}]) {
decode: (value) => switch (value[${dartStringLiteral(node.discriminatorKey)}]) {
${decodeCases.join(',\n ')},
final unknown => throw StateError(
'Unknown ${node.discriminatorKey}: \$unknown',
Expand Down Expand Up @@ -253,9 +254,9 @@ abstract final class ${metadata.facadeName} {
if (node.discriminatorKey case final key?)
if (node.discriminatorValue case final value?)
if (!node.fields.any((field) => field.jsonKey == key))
'${_literal(key)}: ${_ack('Ack')}.literal(${_literal(value)}).optional()',
'${dartStringLiteral(key)}: ${_ack('Ack')}.literal(${dartStringLiteral(value)}).optional()',
for (final field in node.fields)
'${_literal(field.jsonKey)}: ${field.schemaExpression}',
'${dartStringLiteral(field.jsonKey)}: ${field.schemaExpression}',
];
final additional = node.allowsUnknownProperties
? ', additionalProperties: true'
Expand All @@ -279,7 +280,7 @@ ${node.className} $function(Map<String, Object?> value) {
const declared = ${_keySet(declared)};
return $helper(<String, dynamic>{
...value,
${_literal(captureJsonKey)}: Map<String, Object?>.fromEntries(
${dartStringLiteral(captureJsonKey)}: Map<String, Object?>.fromEntries(
value.entries.where((entry) => !declared.contains(entry.key)),
),
});
Expand Down Expand Up @@ -313,10 +314,10 @@ Map<String, Object?> $function(${node.className} model) =>
..add('final result = <String, Object?>{...$jsonHelper(model)};')
..addAll([
if (captureJsonKey != null)
'result.remove(${_literal(captureJsonKey)});',
'result.remove(${dartStringLiteral(captureJsonKey)});',
for (final field in requiredNulls)
'if (model.${field.dartName} == null) { '
'result[${_literal(field.jsonKey)}] = null; }',
'result[${dartStringLiteral(field.jsonKey)}] = null; }',
]);

final entries = <String>[
Expand All @@ -325,7 +326,7 @@ Map<String, Object?> $function(${node.className} model) =>
' if (!declared.contains(entry.key)) entry.key: entry.value',
'...result',
if (discriminatorKey != null && discriminatorValue != null)
'${_literal(discriminatorKey)}: ${_literal(discriminatorValue)}',
'${dartStringLiteral(discriminatorKey)}: ${dartStringLiteral(discriminatorValue)}',
];
lines.add('return <String, Object?>{${entries.join(',\n ')}};');
return '''
Expand Down Expand Up @@ -456,7 +457,7 @@ Map<String, Object?> $function(${node.className} model) {
};

String _keySet(Set<String> keys) =>
'<String>{${keys.map(_literal).join(', ')}}';
'<String>{${keys.map(dartStringLiteral).join(', ')}}';

AckClassModelMetadata _metadata(AckModelGraph graph, AckModelNode node) {
final metadata = graph.classMetadataFor(node.id);
Expand All @@ -470,12 +471,4 @@ Map<String, Object?> $function(${node.className} model) {
final prefix = ackPrefix;
return prefix == null || prefix.isEmpty ? symbol : '$prefix.$symbol';
}

String _literal(String value) {
final escaped = value
.replaceAll(r'\', r'\\')
.replaceAll("'", r"\'")
.replaceAll(r'$', r'\$');
return "'$escaped'";
}
}
27 changes: 10 additions & 17 deletions packages/ack_generator/lib/src/builders/model_emitter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:code_builder/code_builder.dart';
import '../json/helper_names.dart';
import '../models/schema_model_graph.dart';
import 'data_class_emitter.dart';
import '../utils/string_literal.dart';

/// Emits immutable model declarations solely from a normalized model graph.
final class AckModelEmitter {
Expand Down Expand Up @@ -172,7 +173,7 @@ final class AckModelEmitter {
for (final entry in node.branches.entries) {
final branch = nodes[entry.value]!;
cases.add(
'${_literal(entry.key)} => ${branch.className}._fromAckRuntime(value)',
'${dartStringLiteral(entry.key)} => ${branch.className}._fromAckRuntime(value)',
);
}
return Class(
Expand Down Expand Up @@ -223,7 +224,7 @@ final class AckModelEmitter {
),
)
..body = Code('''
return switch (value[${_literal(node.discriminatorKey)}]) {
return switch (value[${dartStringLiteral(node.discriminatorKey)}]) {
${cases.join(',\n ')},
final unknown => throw StateError(
'Unknown ${node.discriminatorKey}: \$unknown',
Expand Down Expand Up @@ -255,7 +256,7 @@ return switch (value[${_literal(node.discriminatorKey)}]) {
if (node.additionalProperties) _additionalPropertiesField(),
_adapter(
node,
'${union.id.declarationName}.effectiveBranch(${_literal(value)})',
'${union.id.declarationName}.effectiveBranch(${dartStringLiteral(value)})',
),
if (_exposesSchemaShorthand(
declarationName: union.id.declarationName,
Expand All @@ -278,7 +279,7 @@ return switch (value[${_literal(node.discriminatorKey)}]) {
..name = discriminator
..returns = refer('String')
..lambda = true
..body = Code(_literal(value)),
..body = Code(dartStringLiteral(value)),
),
..._valueMembers(node.className, _objectDataClass(node)),
_objectFromRuntime(
Expand All @@ -289,7 +290,7 @@ return switch (value[${_literal(node.discriminatorKey)}]) {
_objectToRuntime(
node,
fields: fields,
leadingEntries: {discriminator: _literal(value)},
leadingEntries: {discriminator: dartStringLiteral(value)},
isOverride: true,
),
..._fieldBridges(fields),
Expand Down Expand Up @@ -765,7 +766,7 @@ return $helper(<String, dynamic>{
if (!needsBlock) {
final entries = <String>[
for (final entry in leadingEntries.entries)
'${_literal(entry.key)}: ${entry.value}',
'${dartStringLiteral(entry.key)}: ${entry.value}',
'...$helper(this)',
];
m
Expand All @@ -782,7 +783,7 @@ return $helper(<String, dynamic>{
for (final field in requiredNulls) {
lines.add(
'if (${field.dartName} == null) {'
' result[${_literal(field.jsonKey)}] = null;'
' result[${dartStringLiteral(field.jsonKey)}] = null;'
' }',
);
}
Expand All @@ -791,7 +792,7 @@ return $helper(<String, dynamic>{
'for (final entry in additionalProperties.entries)\n'
' if (!declared.contains(entry.key)) entry.key: entry.value',
for (final entry in leadingEntries.entries)
'${_literal(entry.key)}: ${entry.value}',
'${dartStringLiteral(entry.key)}: ${entry.value}',
'...result',
];
lines.add(
Expand Down Expand Up @@ -1022,7 +1023,7 @@ return $helper(<String, dynamic>{
}) => {...additionalKeys, for (final field in fields) field.jsonKey};

String _declaredKeysLiteral(Set<String> keys) =>
'<String>{${keys.map(_literal).join(', ')}}';
'<String>{${keys.map(dartStringLiteral).join(', ')}}';

AckInferRef _nonNullable(AckInferRef type) => switch (type) {
AckNullableTypeRef(:final inner) => inner,
Expand Down Expand Up @@ -1087,14 +1088,6 @@ return $helper(<String, dynamic>{
return prefix == null || prefix.isEmpty ? symbol : '$prefix.$symbol';
}

String _literal(String value) {
final escaped = value
.replaceAll(r'\', r'\\')
.replaceAll("'", r"\'")
.replaceAll(r'$', r'\$');
return "'$escaped'";
}

static const _runtimeMapType = 'Map<String, Object?>';
static const _runtimeMapLiteral = '<String, Object?>';
static const _objectJsonType = 'Map<String, dynamic>';
Expand Down
13 changes: 3 additions & 10 deletions packages/ack_generator/lib/src/inference/schema_inference.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:analyzer/dart/element/type.dart';
import 'package:source_gen/source_gen.dart';

import '../utils/doc_comment_utils.dart';
import '../utils/string_literal.dart';

/// Infers Ack schema expressions from resolved Dart types and annotations.
///
Expand Down Expand Up @@ -185,7 +186,7 @@ final class AckSchemaInference {
} else if (_pattern.isExactlyType(valueType)) {
_require(declaration, type, '@Pattern', isString, 'String field');
output =
'$output.matches(${_literal(value.getField('pattern')!.toStringValue()!)})';
'$output.matches(${dartStringLiteral(value.getField('pattern')!.toStringValue()!)})';
} else if (_email.isExactlyType(valueType)) {
_require(declaration, type, '@Email', isString, 'String field');
output = '$output.email()';
Expand Down Expand Up @@ -282,7 +283,7 @@ final class AckSchemaInference {
);
return description == null
? schema
: '$schema.describe(${_literal(description)})';
: '$schema.describe(${dartStringLiteral(description)})';
}

String? _scalar(InterfaceType type) {
Expand Down Expand Up @@ -319,14 +320,6 @@ final class AckSchemaInference {
return (number.toIntValue() ?? number.toDoubleValue())!.toString();
}

String _literal(String value) {
final escaped = value
.replaceAll(r'\', r'\\')
.replaceAll("'", r"\'")
.replaceAll(r'$', r'\$');
return "'$escaped'";
}

void _require(
Element declaration,
DartType type,
Expand Down
16 changes: 16 additions & 0 deletions packages/ack_generator/lib/src/utils/string_literal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// Returns [value] as a single-quoted Dart string literal.
///
/// Escapes backslashes, quotes, and `$`, and writes control characters and
/// Unicode line separators as `\uXXXX`, so generated source stays valid and
/// keeps the original string value.
String dartStringLiteral(String value) {
final escaped = value
.replaceAll(r'\', r'\\')
.replaceAll("'", r"\'")
.replaceAll(r'$', r'\$')
.replaceAllMapped(RegExp(r'[\x00-\x1f

]'), (match) {
final hex = match[0]!.codeUnitAt(0).toRadixString(16).padLeft(4, '0');
return '\\u$hex';
});
return "'$escaped'";
}
Loading
Loading