Tweak formatting rules. Fix #211.
Discard open spans when a hard split occurs.
This is a correctness issue, not a performance one. The previous
code would leave spans open even when a hard split occurred in
them. These were mostly innocuous since they ended up with
totally bogus ranges, but they apparently do affect some
corner-case formatting.
Some of this was triggered by the previous patch which was for
perf but also made some stuff more correct.
BUG=https://github.com/dart-lang/dart_style/issues/
R=pquitslund@google.com
Review URL: https://chromiumcodereview.appspot.com//1000513002
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10ba9d2..78a6042 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,10 @@
* Update to latest analyzer (#177).
* Don't discard annotations on initializing formals (#197).
* Optimize formatting deeply nested expressions (#108).
+* Discard unused nesting level to improve performance (#108).
+* Discard unused spans to improve performance (#108).
+* Try to avoid splitting single-element lists (#211).
+* Avoid splitting when the first argument is a function expression (#211).
# 0.1.6
diff --git a/lib/src/chunk.dart b/lib/src/chunk.dart
index e060427..307cd95 100644
--- a/lib/src/chunk.dart
+++ b/lib/src/chunk.dart
@@ -230,9 +230,19 @@
/// greater number of nested spans.
static const normal = 1;
- /// The cost of splitting after a "=" both for assignment and initialization.
+ /// Splitting after a "=" both for assignment and initialization.
static const assignment = 2;
+ /// Splitting before the first argument when it happens to be a function
+ /// expression with a block body.
+ static const firstBlockArgument = 2;
+
+ /// The series of positional arguments.
+ static const positionalArguments = 2;
+
+ /// Splitting inside the brackets of a list with only one element.
+ static const singleElementList = 2;
+
/// The cost of a single character that goes past the page limit.
///
/// This cost is high to ensure any solution that fits in the page is
@@ -267,7 +277,8 @@
final implies = <SplitParam>[];
/// Creates a new [SplitParam].
- SplitParam([this.cost = Cost.normal]);
+ SplitParam([int cost])
+ : cost = cost != null ? cost : Cost.normal;
String toString() => "$id";
diff --git a/lib/src/line_writer.dart b/lib/src/line_writer.dart
index 0477e2f..3169aaa 100644
--- a/lib/src/line_writer.dart
+++ b/lib/src/line_writer.dart
@@ -74,7 +74,7 @@
/// The nested stack of spans that are currently being written.
final _openSpans = <Span>[];
- /// All of the spans that have been created, open and closed.
+ /// All of the spans that have been created and closed.
final _spans = <Span>[];
/// The current indentation and nesting levels.
@@ -371,6 +371,10 @@
/// Ends the innermost span.
void endSpan() {
var span = _openSpans.removeLast();
+
+ // If the span was discarded while it was still open, just forget about it.
+ if (span == null) return;
+
span.close(_currentChunkIndex);
// A span that just covers a single chunk can't be split anyway.
@@ -381,8 +385,9 @@
/// Starts a new [Multisplit].
///
/// Returns the [SplitParam] for the multisplit.
- SplitParam startMultisplit({bool separable}) {
- var multisplit = new Multisplit(_currentChunkIndex, separable: separable);
+ SplitParam startMultisplit({bool separable, int cost}) {
+ var multisplit = new Multisplit(_currentChunkIndex,
+ separable: separable, cost: cost);
_multisplits.add(multisplit);
return multisplit.param;
@@ -423,7 +428,7 @@
/// In particular, it's easy for the visitor to know that collections with a
/// large number of items must split. Doing that early avoids crashing the
/// splitter when it tries to recurse on huge collection literals.
- void preemptMultisplits() => _splitMultisplits();
+ void preemptMultisplits() => _handleHardSplit();
/// Increases the level of expression nesting.
///
@@ -622,7 +627,7 @@
_chunks.last.applySplit(indent, nesting, param,
isDouble: isDouble, spaceWhenUnsplit: spaceWhenUnsplit);
- if (_chunks.last.isHardSplit) _splitMultisplits();
+ if (_chunks.last.isHardSplit) _handleHardSplit();
}
/// Writes [text] to either the current chunk or a new one if the current
@@ -677,11 +682,6 @@
void _completeLine(int length) {
assert(_chunks.isNotEmpty);
- if (debugFormatter) {
- dumpChunks(_chunks.take(length).toList());
- print(_spans.join("\n"));
- }
-
// Write the newlines required by the previous line.
for (var i = 0; i < _bufferedNewlines; i++) {
_buffer.write(_formatter.lineEnding);
@@ -697,6 +697,11 @@
spans = spans.where((span) => span.start <= length).toList();
}
+ if (debugFormatter) {
+ dumpChunks(chunks);
+ print(spans.join("\n"));
+ }
+
var splitter = new LineSplitter(_formatter.lineEnding, _formatter.pageWidth,
chunks, spans, _beginningIndent);
var selection = splitter.apply(_buffer);
@@ -705,13 +710,21 @@
if (selection[1] != null) _selectionLength = selection[1] - _selectionStart;
}
- /// Handles multisplits when a hard line occurs.
+ /// Handles open multisplits and spans when a hard line occurs.
///
/// Any active separable multisplits will get split in two at this point.
/// Other multisplits are forced into the "hard" state. All of their previous
/// splits are turned into explicit hard splits and any new splits for that
/// multisplit become hard splits too.
- void _splitMultisplits() {
+ ///
+ /// All open spans get discarded since they will never be satisfied.
+ void _handleHardSplit() {
+ // Discard all open spans. We don't remove them from the stack so that we
+ // can still correctly count later calls to endSpan().
+ for (var i = 0; i < _openSpans.length; i++) {
+ _openSpans[i] = null;
+ }
+
if (_multisplits.isEmpty) return;
var splitParams = new Set();
diff --git a/lib/src/multisplit.dart b/lib/src/multisplit.dart
index fa2bf2c..6ad598d 100644
--- a/lib/src/multisplit.dart
+++ b/lib/src/multisplit.dart
@@ -53,15 +53,16 @@
/// The [SplitParam] that controls all of the split chunks.
SplitParam get param => _param;
- SplitParam _param = new SplitParam();
+ SplitParam _param;
/// `true` if a hard newline has forced this multisplit to be split.
bool _isSplit = false;
final bool _separable;
- Multisplit(this.startChunk, {bool separable})
- : _separable = separable != null ? separable : false;
+ Multisplit(this.startChunk, {bool separable, int cost})
+ : _separable = separable != null ? separable : false,
+ _param = new SplitParam(cost);
/// Handles a hard split occurring in the middle of this multisplit.
///
@@ -78,7 +79,7 @@
_isSplit = true;
if (_separable) {
- _param = new SplitParam(param.cost);
+ _param = new SplitParam(_param.cost);
// Previous splits may still remain unsplit.
return null;
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index 4fc1918..ec9ca70 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -105,13 +105,51 @@
// Nest around the parentheses in case there are comments before or after
// them.
_writer.nestExpression();
+
token(node.leftParenthesis);
+ // Corner case: If the first argument to a method is a block-bodied
+ // function, it looks bad if its parameter list gets wrapped to the next
+ // line. Bump the cost to try to avoid that. This prefers:
+ //
+ // receiver
+ // .method()
+ // .chain((parameter, list) {
+ // ...
+ // });
+ //
+ // over:
+ //
+ // receiver.method().chain(
+ // (parameter, list) {
+ // ...
+ // });
+ // TODO(rnystrom): This causes a function expression's long parameter list
+ // to get split instead, like:
+ //
+ // receiver.method((longParameter,
+ // anotherParameter) {
+ // ...
+ // });
+ //
+ // Instead of bumping the cost, this should wrap a span around the "("
+ // before the argument list and the function's parameter list. That requires
+ // spans to not strictly be a stack, though, so would be a larger change
+ // than I want to do right now.
+ var cost = Cost.normal;
+ if (node.arguments.isNotEmpty) {
+ var firstArg = node.arguments.first;
+ if (firstArg is FunctionExpression &&
+ firstArg.body is BlockFunctionBody) {
+ cost = Cost.firstBlockArgument;
+ }
+ }
+
// Allow splitting after "(".
- var lastParam = zeroSplit();
+ var lastParam = zeroSplit(cost);
// Try to keep the positional arguments together.
- _writer.startSpan();
+ _writer.startSpan(Cost.positionalArguments);
var i = 0;
for (; i < node.arguments.length; i++) {
@@ -979,8 +1017,11 @@
}
visitListLiteral(ListLiteral node) {
+ // Corner case: Splitting inside a list looks bad if there's only one
+ // element, so make those more costly.
+ var cost = node.elements.length <= 1 ? Cost.singleElementList : Cost.normal;
_visitCollectionLiteral(
- node, node.leftBracket, node.elements, node.rightBracket);
+ node, node.leftBracket, node.elements, node.rightBracket, cost);
}
visitMapLiteral(MapLiteral node) {
@@ -1542,11 +1583,11 @@
/// Visits the collection literal [node] whose body starts with [leftBracket],
/// ends with [rightBracket] and contains [elements].
void _visitCollectionLiteral(TypedLiteral node, Token leftBracket,
- Iterable<AstNode> elements, Token rightBracket) {
+ Iterable<AstNode> elements, Token rightBracket, [int cost]) {
modifier(node.constKeyword);
visit(node.typeArguments);
- _startBody(leftBracket);
+ _startBody(leftBracket, cost: cost);
// Each list element takes at least 3 characters (one character for the
// element, one for the comma, one for the space), so force it to split if
@@ -1649,11 +1690,11 @@
///
/// If [space] is `true`, then the initial multisplit will use a space if not
/// split.
- void _startBody(Token leftBracket, {bool space: false}) {
+ void _startBody(Token leftBracket, {int cost, bool space: false}) {
token(leftBracket);
// Indent the body.
- _writer.startMultisplit();
+ _writer.startMultisplit(cost: cost);
_writer.indent();
// Split after the bracket.
@@ -1746,7 +1787,7 @@
/// Writes a split that is the empty string when unsplit.
///
/// Returns the newly created [SplitParam].
- SplitParam zeroSplit() => _writer.writeSplit();
+ SplitParam zeroSplit([int cost]) => _writer.writeSplit(cost: cost);
/// Emit [token], along with any comments and formatted whitespace that comes
/// before it.
diff --git a/test/regression/121.stmt b/test/regression/121.stmt
index b381bdf..e834810 100644
--- a/test/regression/121.stmt
+++ b/test/regression/121.stmt
@@ -14,7 +14,9 @@
if (!firstArgument.isString) {
DartType type = defaultValue.getType(compiler.coreTypes);
compiler.reportFatalError(send.arguments.head,
- MessageKind.NOT_ASSIGNABLE,
- {'fromType': type, 'toType': compiler.stringClass.rawType});
+ MessageKind.NOT_ASSIGNABLE, {
+ 'fromType': type,
+ 'toType': compiler.stringClass.rawType
+ });
return null;
}
\ No newline at end of file
diff --git a/test/regression/211.unit b/test/regression/211.unit
new file mode 100644
index 0000000..721bc1e
--- /dev/null
+++ b/test/regression/211.unit
@@ -0,0 +1,69 @@
+>>> (indent 2)
+ LiteralMap mapLiteral(List<LiteralMapEntry> entries, {bool isConst: false}) {
+ return new LiteralMap(null, // Type arguments.
+ new NodeList(symbolToken(OPEN_CURLY_BRACKET_INFO),
+ linkedList(entries), symbolToken(CLOSE_CURLY_BRACKET_INFO), ','),
+ isConst ? keywordToken('const') : null);
+ }
+<<<
+ LiteralMap mapLiteral(List<LiteralMapEntry> entries, {bool isConst: false}) {
+ return new LiteralMap(null, // Type arguments.
+ new NodeList(symbolToken(OPEN_CURLY_BRACKET_INFO), linkedList(entries),
+ symbolToken(CLOSE_CURLY_BRACKET_INFO), ','),
+ isConst ? keywordToken('const') : null);
+ }
+>>>
+void defineProperty(var obj, String property, var value) {
+ JS('void', 'Object.defineProperty(#, #, '
+ '{value: #, enumerable: false, writable: true, configurable: true})',
+ obj, property, value);
+}
+<<<
+void defineProperty(var obj, String property, var value) {
+ JS('void', 'Object.defineProperty(#, #, '
+ '{value: #, enumerable: false, writable: true, configurable: true})', obj,
+ property, value);
+}
+>>> (indent 4)
+ main() {
+ return searchEngine.searchTopLevelDeclarations('').then(
+ (List<SearchMatch> matches) {
+ _addNonImportedElementSuggestions(matches, excludedLibs);
+ return true;
+ });
+ }
+<<<
+ main() {
+ return searchEngine
+ .searchTopLevelDeclarations('')
+ .then((List<SearchMatch> matches) {
+ _addNonImportedElementSuggestions(matches, excludedLibs);
+ return true;
+ });
+ }
+>>> (indent 2)
+ test_getRelationships_empty() {
+ return store.getRelationships(elementA, relationship).then(
+ (List<Location> locations) {
+ expect(locations, isEmpty);
+ });
+ }
+<<<
+ test_getRelationships_empty() {
+ return store
+ .getRelationships(elementA, relationship)
+ .then((List<Location> locations) {
+ expect(locations, isEmpty);
+ });
+ }
+>>> (indent 2)
+ _asRuntimeType() {
+ return [
+ _class._jsConstructor
+ ].addAll(typeArguments.map((t) => t._asRuntimeType()));
+ }
+<<<
+ _asRuntimeType() {
+ return [_class._jsConstructor]
+ .addAll(typeArguments.map((t) => t._asRuntimeType()));
+ }
\ No newline at end of file