Optimize formatting large cascades.
Fix #811.
diff --git a/benchmark/after.dart.txt b/benchmark/after.dart.txt
index f72bd3d..8b8ec92 100644
--- a/benchmark/after.dart.txt
+++ b/benchmark/after.dart.txt
@@ -22,6 +22,96 @@
import 'version_queue.dart';
import 'version_solver.dart';
+class HttpRequest extends $pb.GeneratedMessage {
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ const $core.bool.fromEnvironment('protobuf.omit_message_names')
+ ? ''
+ : 'HttpRequest',
+ package: const $pb.PackageName(
+ const $core.bool.fromEnvironment('protobuf.omit_message_names')
+ ? ''
+ : 'google.logging.type'),
+ createEmptyInstance: create)
+ ..aOS(
+ 1,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'requestMethod')
+ ..aOS(
+ 2,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'requestUrl')
+ ..aInt64(
+ 3,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'requestSize')
+ ..a<$core.int>(
+ 4,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'status',
+ $pb.PbFieldType.O3)
+ ..aInt64(
+ 5,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'responseSize')
+ ..aOS(
+ 6,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'userAgent')
+ ..aOS(
+ 7,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'remoteIp')
+ ..aOS(
+ 8,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'referer')
+ ..aOB(
+ 9,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'cacheHit')
+ ..aOB(
+ 10,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'cacheValidatedWithOriginServer')
+ ..aOB(
+ 11,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'cacheLookup')
+ ..aInt64(
+ 12,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'cacheFillBytes')
+ ..aOS(
+ 13,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'serverIp')
+ ..aOM<$0.Duration>(
+ 14,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'latency',
+ subBuilder: $0.Duration.create)
+ ..aOS(
+ 15,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'protocol')
+ ..hasRequiredFields = false;
+}
+
/// The top-level solver.
///
/// Keeps track of the current potential solution, and the other possible
diff --git a/benchmark/before.dart.txt b/benchmark/before.dart.txt
index a542414..25f129a 100644
--- a/benchmark/before.dart.txt
+++ b/benchmark/before.dart.txt
@@ -22,6 +22,96 @@
import 'version_queue.dart';
import 'version_solver.dart';
+class HttpRequest extends $pb.GeneratedMessage {
+ static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+ const $core.bool.fromEnvironment('protobuf.omit_message_names')
+ ? ''
+ : 'HttpRequest',
+ package: const $pb.PackageName(
+ const $core.bool.fromEnvironment('protobuf.omit_message_names')
+ ? ''
+ : 'google.logging.type'),
+ createEmptyInstance: create)
+ ..aOS(
+ 1,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'requestMethod')
+ ..aOS(
+ 2,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'requestUrl')
+ ..aInt64(
+ 3,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'requestSize')
+ ..a<$core.int>(
+ 4,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'status',
+ $pb.PbFieldType.O3)
+ ..aInt64(
+ 5,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'responseSize')
+ ..aOS(
+ 6,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'userAgent')
+ ..aOS(
+ 7,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'remoteIp')
+ ..aOS(
+ 8,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'referer')
+ ..aOB(
+ 9,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'cacheHit')
+ ..aOB(
+ 10,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'cacheValidatedWithOriginServer')
+ ..aOB(
+ 11,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'cacheLookup')
+ ..aInt64(
+ 12,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'cacheFillBytes')
+ ..aOS(
+ 13,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'serverIp')
+ ..aOM<$0.Duration>(
+ 14,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'latency',
+ subBuilder: $0.Duration.create)
+ ..aOS(
+ 15,
+ const $core.bool.fromEnvironment('protobuf.omit_field_names')
+ ? ''
+ : 'protocol')
+ ..hasRequiredFields = false;
+}
+
/// The top-level solver.
///
/// Keeps track of the current potential solution, and the other possible
diff --git a/lib/src/chunk.dart b/lib/src/chunk.dart
index 90d5636..83ec33d 100644
--- a/lib/src/chunk.dart
+++ b/lib/src/chunk.dart
@@ -225,9 +225,9 @@
}
/// Turns this chunk into one that can contain a block of child chunks.
- void makeBlock(Chunk? blockArgument) {
+ void makeBlock(Chunk? blockArgument, {required bool indent}) {
assert(_block == null);
- _block = ChunkBlock(blockArgument);
+ _block = ChunkBlock(blockArgument, indent);
}
/// Returns `true` if the block body owned by this chunk should be expression
@@ -290,10 +290,13 @@
/// may need extra expression-level indentation.
final Chunk? argument;
+ /// Whether the first chunk should have a level of indentation before it.
+ final bool indent;
+
/// The child chunks in this block.
final List<Chunk> chunks = [];
- ChunkBlock(this.argument);
+ ChunkBlock(this.argument, this.indent);
}
/// Constants for the cost heuristics used to determine which set of splits is
diff --git a/lib/src/chunk_builder.dart b/lib/src/chunk_builder.dart
index c2ab67b..fb2317b 100644
--- a/lib/src/chunk_builder.dart
+++ b/lib/src/chunk_builder.dart
@@ -591,14 +591,12 @@
/// Starts a new block as a child of the current chunk.
///
/// Nested blocks are handled using their own independent [LineWriter].
- ChunkBuilder startBlock(Chunk? argumentChunk) {
+ ChunkBuilder startBlock(Chunk? argumentChunk, {bool indent = true}) {
var chunk = _chunks.last;
- chunk.makeBlock(argumentChunk);
+ chunk.makeBlock(argumentChunk, indent: indent);
var builder = ChunkBuilder._(this, _formatter, _source, chunk.block.chunks);
-
- // A block always starts off indented one level.
- builder.indent();
+ if (indent) builder.indent();
return builder;
}
diff --git a/lib/src/line_writer.dart b/lib/src/line_writer.dart
index e8874a8..13f883d 100644
--- a/lib/src/line_writer.dart
+++ b/lib/src/line_writer.dart
@@ -84,7 +84,8 @@
// TODO(rnystrom): Passing in an initial indent here is hacky. The
// LineWriter ensures all but the first chunk have a block indent, and this
// handles the first chunk. Do something cleaner.
- var result = writer.writeLines(Indent.block, flushLeft: chunk.flushLeft);
+ var result = writer.writeLines(chunk.block.indent ? Indent.block : 0,
+ flushLeft: chunk.flushLeft);
return _blockCache[key] = result;
}
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index bd8da7b..b03a87d 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -564,6 +564,12 @@
@override
void visitCascadeExpression(CascadeExpression node) {
+ // Optimized path if we know the cascade will split.
+ if (node.cascadeSections.length > 1) {
+ _visitSplitCascade(node);
+ return;
+ }
+
// Whether a split in the cascade target expression forces the cascade to
// move to the next line. It looks weird to move the cascade down if the
// target expression is a collection, so we don't:
@@ -605,6 +611,90 @@
builder.unnest();
}
+ /// Format the cascade using a nested block instead of a single inline
+ /// expression.
+ ///
+ /// If the cascade has multiple sections, we know each section will be on its
+ /// own line and we know there will be at least one trailing section following
+ /// a preceding one. That let's us treat all of the earlier sections as a
+ /// separate block like we do with collections and functions, instead of a
+ /// monolithic expression. Using a block in turn makes big cascades much
+ /// faster to format (like 10x) since the block formatting is memoized and
+ /// each cascade section in it is formatted independently.
+ ///
+ /// The tricky part is that block formatting assumes the entire line will be
+ /// part of the block. This is not true of the last section in a cascade,
+ /// which may have other trailing code, like the `;` here:
+ ///
+ /// var x = someLeadingExpression
+ /// ..firstCascade()
+ /// ..secondCascade()
+ /// ..thirdCascade()
+ /// ..fourthCascade();
+ ///
+ /// To handle that, we don't put the last section in the block and instead
+ /// format it with the surrounding expression. So, from the formatter's
+ /// view, a cascade like:
+ ///
+ /// var x = someLeadingExpression
+ /// ..firstCascade()
+ /// ..secondCascade()
+ /// ..thirdCascade()
+ /// ..fourthCascade();
+ ///
+ /// Is formatted like:
+ ///
+ /// var x = someLeadingExpression
+ /// [ begin block ]
+ /// ..firstCascade()
+ /// ..secondCascade()
+ /// ..thirdCascade()
+ /// [ end block ]
+ /// ..fourthCascade();
+ ///
+ /// This somewhere between clever and hacky, but it works and allows cascades
+ /// of essentially unbounded length to be formatted quickly.
+ void _visitSplitCascade(CascadeExpression node) {
+ // Rule to split before the first `..`.
+ builder.startLazyRule(Rule.hard());
+ visit(node.target);
+
+ builder.nestExpression(indent: Indent.cascade, now: true);
+ builder.startBlockArgumentNesting();
+
+ zeroSplit();
+
+ // If there are comments before the first section, keep them outside of the
+ // block.
+ var firstCommentToken = node.cascadeSections.first.beginToken;
+ writePrecedingCommentsAndNewlines(firstCommentToken);
+ _suppressPrecedingCommentsAndNewLines.add(firstCommentToken);
+
+ // Process the inner cascade sections as a separate block. This way the
+ // entire cascade expression isn't line split as a single monolithic unit,
+ // which is very slow.
+ builder = builder.startBlock(null, indent: false);
+
+ for (var i = 0; i < node.cascadeSections.length - 1; i++) {
+ if (i > 0) newline();
+ visit(node.cascadeSections[i]);
+ }
+
+ // Put comments before the last section inside the block.
+ var lastCommentToken = node.cascadeSections.last.beginToken;
+ writePrecedingCommentsAndNewlines(lastCommentToken);
+ _suppressPrecedingCommentsAndNewLines.add(lastCommentToken);
+
+ builder = builder.endBlock(null, forceSplit: true);
+
+ // The last section is outside of the block.
+ visit(node.cascadeSections.last);
+
+ builder.endRule();
+ builder.endBlockArgumentNesting();
+ builder.unnest();
+ }
+
/// Whether [expression] is a collection literal, or a call with a trailing
/// comma in an argument list.
///
@@ -640,9 +730,13 @@
!hasCommaAfter(arguments.arguments.last);
}
- /// Whether a cascade should be allowed to be inline as opposed to one
- /// expression per line.
+ /// Whether a cascade should be allowed to be inline as opposed to moving the
+ /// section to the next line.
bool _allowInlineCascade(CascadeExpression node) {
+ // Cascades with multiple sections are handled elsewhere and are never
+ // inline.
+ assert(node.cascadeSections.length == 1);
+
// If the receiver is an expression that makes the cascade's very low
// precedence confusing, force it to split. For example:
//
@@ -654,9 +748,7 @@
if (node.target is PrefixExpression) return false;
if (node.target is AwaitExpression) return false;
- // Only allow a single cascade to be inline.
- if (node.cascadeSections.length < 2) return true;
- return false;
+ return true;
}
@override
diff --git a/test/comments/cascades.stmt b/test/comments/cascades.stmt
new file mode 100644
index 0000000..ab01fe6
--- /dev/null
+++ b/test/comments/cascades.stmt
@@ -0,0 +1,67 @@
+>>> comments on single cascade lines
+receiver..cascade(); // comment
+<<<
+receiver..cascade(); // comment
+>>> comments on split cascade lines
+receiver
+ ..cascade() // a
+ ..cascade() // b
+ ..more(); // c
+<<<
+receiver
+ ..cascade() // a
+ ..cascade() // b
+ ..more(); // c
+>>> comment before first multi-line cascade section stays on line
+receiver // comment
+ ..cascade()
+ ..more();
+<<<
+receiver // comment
+ ..cascade()
+ ..more();
+>>> collapse blank lines before comment before first cascade
+receiver
+
+
+
+
+ // comment
+ ..cascade()
+ ..cascade();
+<<<
+receiver
+
+ // comment
+ ..cascade()
+ ..cascade();
+>>> preserve one blank line before comments on other cascades
+receiver
+
+
+
+
+ // comment
+ ..cascade()
+ // no blank
+ ..cascade()
+
+ // comment
+ ..cascade()
+
+
+ // comment
+ ..cascade();
+<<<
+receiver
+
+ // comment
+ ..cascade()
+ // no blank
+ ..cascade()
+
+ // comment
+ ..cascade()
+
+ // comment
+ ..cascade();
\ No newline at end of file
diff --git a/test/regression/0800/0881.unit b/test/regression/0800/0881.unit
new file mode 100644
index 0000000..f420d35
--- /dev/null
+++ b/test/regression/0800/0881.unit
@@ -0,0 +1,160 @@
+>>>
+void main() async {
+ group('my group', () {
+ setUp(() async {
+ final requestHandler = FakeRequestHandler()
+ ..when(withServiceName(Ng2ProtoFooBarBazService.serviceName))
+ .thenRespond(FooBarBazListResponse()
+ ..entities.add(FooBarBaz()
+ ..fooBarBazEntityId = entityId
+ ..name = 'Test entity'
+ ..baseFooBarBazId = baseFooBarBazId
+ ..entityFooBarBazId = entityFooBarBazId))
+ ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+ hasFooBarBazId(baseFooBarBazId)))
+ .thenRespond(FooBarBazListResponse()..entities.add(baseFooBarBaz))
+ ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+ hasFooBarBazId(entityFooBarBazId)))
+ .thenRespond(FooBarBazListResponse()..entities.add(entityFooBarBaz))
+ ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+ hasFooBarBazIds(Set.from([baseFooBarBazId, entityFooBarBazId]))))
+ .thenRespond(
+ FooBarBazListResponse()..entities.addAll([baseFooBarBaz, entityFooBarBaz]));
+ });
+ });
+}
+<<<
+void main() async {
+ group('my group', () {
+ setUp(() async {
+ final requestHandler = FakeRequestHandler()
+ ..when(withServiceName(Ng2ProtoFooBarBazService.serviceName))
+ .thenRespond(FooBarBazListResponse()
+ ..entities.add(FooBarBaz()
+ ..fooBarBazEntityId = entityId
+ ..name = 'Test entity'
+ ..baseFooBarBazId = baseFooBarBazId
+ ..entityFooBarBazId = entityFooBarBazId))
+ ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+ hasFooBarBazId(baseFooBarBazId)))
+ .thenRespond(FooBarBazListResponse()..entities.add(baseFooBarBaz))
+ ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+ hasFooBarBazId(entityFooBarBazId)))
+ .thenRespond(FooBarBazListResponse()..entities.add(entityFooBarBaz))
+ ..when(allOf(
+ withServiceName(Ng2ProtoFooBarBazService.serviceName),
+ hasFooBarBazIds(
+ Set.from([baseFooBarBazId, entityFooBarBazId])))).thenRespond(
+ FooBarBazListResponse()
+ ..entities.addAll([baseFooBarBaz, entityFooBarBaz]));
+ });
+ });
+}
+>>>
+aaaa() {
+ {
+ {
+ {
+ aaaaa aaaaa = AaaAaaaa.aaaaaaa().aaaaaaa((a) => a
+ ..aaaaAaaaa
+ .aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa.AAA_AAAAAAAAAAA_AAAAA_AAAAAAA.aaaa] =
+ aaaa
+ ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+ .AAA_AAAAAAAAAAA_AAAAAAAA_AAAA_AAAAAAAAA.aaaa] = aaaa
+ ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+ .AAA_AAAAAAAAAAA_AAAAAAA_AAAA_AAAAAAAAA.aaaa] = aaaa
+ ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+ .AAA_AAAAAAAAAAA_AAAAAA_AAAAAAA_AAA_AAAAAAAAAAA_AAAAA.aaaa] = aaaa
+ ..aaaaaaaaAaaaaaAaaaa.aaaaaaa(AaaaaaaaAaaaaaAaaaaAaaaa.aaaaaaa())
+ ..aaaaaaaaAaaaa.aaaaaaaa =
+ (Aaaaaaaa()..aaaaaaaaAaaa = (AaaaaaaaAaaa()..aaAaaaaaa = aaaaa)));
+ }
+ }
+ }
+}
+<<<
+aaaa() {
+ {
+ {
+ {
+ aaaaa aaaaa = AaaAaaaa.aaaaaaa().aaaaaaa((a) => a
+ ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[
+ AaaaAaaaaaaaaaAaaa_Aaaa.AAA_AAAAAAAAAAA_AAAAA_AAAAAAA.aaaa] = aaaa
+ ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+ .AAA_AAAAAAAAAAA_AAAAAAAA_AAAA_AAAAAAAAA.aaaa] = aaaa
+ ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+ .AAA_AAAAAAAAAAA_AAAAAAA_AAAA_AAAAAAAAA.aaaa] = aaaa
+ ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+ .AAA_AAAAAAAAAAA_AAAAAA_AAAAAAA_AAA_AAAAAAAAAAA_AAAAA.aaaa] = aaaa
+ ..aaaaaaaaAaaaaaAaaaa.aaaaaaa(AaaaaaaaAaaaaaAaaaaAaaaa.aaaaaaa())
+ ..aaaaaaaaAaaaa.aaaaaaaa =
+ (Aaaaaaaa()..aaaaaaaaAaaa = (AaaaaaaaAaaa()..aaAaaaaaa = aaaaa)));
+ }
+ }
+ }
+}
+>>>
+void main() {
+ Example()
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName());
+}
+<<<
+void main() {
+ Example()
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+ ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+ ClassWithLongName(), ClassWithLongName(), ClassWithLongName());
+}
\ No newline at end of file
diff --git a/test/regression/other/cascades.unit b/test/regression/other/cascades.unit
new file mode 100644
index 0000000..78c7eed
--- /dev/null
+++ b/test/regression/other/cascades.unit
@@ -0,0 +1,48 @@
+>>> over_react-4.1.0/tools/analyzer_plugin/playground/web/pseudo_static_lifecycle.dart
+class C {
+ get defaultProps {
+ return newProps() // This newProps() call should not lint
+ ..addProps(
+ super.defaultProps) // This super.defaultProps access should not lint
+ ..somethingThatCanBeTouched =
+ mcHammer; // This mcHammer access SHOULD lint
+ }
+}
+<<<
+class C {
+ get defaultProps {
+ return newProps() // This newProps() call should not lint
+ ..addProps(
+ super.defaultProps) // This super.defaultProps access should not lint
+ ..somethingThatCanBeTouched =
+ mcHammer; // This mcHammer access SHOULD lint
+ }
+}
+>>> sass-1.32.8/lib/src/executable/options.dart
+class C {
+ static final ArgParser _parser = () {
+ var parser = ArgParser(allowTrailingOptions: true)
+
+ // This is used for compatibility with sass-spec, even though we don't
+ // support setting the precision.
+ ..addOption('precision', hide: true)
+
+ // This is used when testing to ensure that the asynchronous evaluator path
+ // works the same as the synchronous one.
+ ..addFlag('async', hide: true);
+ };
+}
+<<<
+class C {
+ static final ArgParser _parser = () {
+ var parser = ArgParser(allowTrailingOptions: true)
+
+ // This is used for compatibility with sass-spec, even though we don't
+ // support setting the precision.
+ ..addOption('precision', hide: true)
+
+ // This is used when testing to ensure that the asynchronous evaluator path
+ // works the same as the synchronous one.
+ ..addFlag('async', hide: true);
+ };
+}
\ No newline at end of file
diff --git a/tool/change_metrics.dart b/tool/change_metrics.dart
new file mode 100644
index 0000000..ab67523
--- /dev/null
+++ b/tool/change_metrics.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2020, 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.
+import 'dart:io';
+
+/// Calculates the amount of formatting changes in a given directory of code.
+///
+/// This should be run with a path to a directory. That directory should
+/// contain a Git repo. The committed state of the repo should be the formatted
+/// output before the change in question. Then the result of the new formatting
+/// should be unstaged changes.
+///
+/// Uses `git diff --shortstat` to calculate the number of changed lines.
+///
+/// Counts the number of lines of Dart code by reading the files.
+void main(List<String> arguments) {
+ if (arguments.length != 1) {
+ print('Usage: change_metrics.dart <dir>');
+ exit(1);
+ }
+
+ var directory = arguments[0];
+ var totalLines = 0;
+ var totalFiles = 0;
+
+ print('Counting lines...');
+ for (var entry in Directory(directory).listSync(recursive: true)) {
+ if (entry is File && entry.path.endsWith('.dart')) {
+ try {
+ var lines = entry.readAsLinesSync();
+ totalFiles++;
+ totalLines += lines.length;
+ } catch (error) {
+ print('Could not read ${entry.path}:\n$error');
+ }
+ }
+ }
+
+ print('Getting diff stats...');
+ var result = Process.runSync('git', ['diff', '--shortstat'],
+ workingDirectory: directory);
+ if (result.exitCode != 0) {
+ print('Git failure:\n${result.stdout}\n${result.stderr}');
+ exit(1);
+ }
+
+ var stdout = result.stdout as String;
+ var insertions = _parseGitStdout(stdout, r'(\d+) insertions');
+ var deletions = _parseGitStdout(stdout, r'(\d+) deletions');
+ var changes = insertions + deletions;
+
+ print('$totalLines lines in $totalFiles files');
+ print('$insertions insertions + $deletions deletions = $changes changes');
+ var linesPerChange = totalLines / changes;
+ print('1 changed line for every ${linesPerChange.toStringAsFixed(2)} '
+ 'lines of code');
+
+ var changesPerLine = 1000.0 * changes / totalLines;
+ print('${changesPerLine.toStringAsFixed(4)} '
+ 'changed lines for every 1,000 lines of code');
+}
+
+int _parseGitStdout(String stdout, String pattern) {
+ var match = RegExp(pattern).firstMatch(stdout);
+ if (match == null) {
+ print('Could not parse Git output:\n$stdout');
+ exit(1);
+ }
+
+ return int.parse(match[1]!);
+}