Index: packages/dart_style/lib/src/line_splitting/solve_state.dart |
diff --git a/packages/dart_style/lib/src/line_splitting/solve_state.dart b/packages/dart_style/lib/src/line_splitting/solve_state.dart |
index b4197561aa9fc9912e3018ef00391b0d09e9c78c..e1e9168c2a2570e90e5c7b149978e27c1776ab0f 100644 |
--- a/packages/dart_style/lib/src/line_splitting/solve_state.dart |
+++ b/packages/dart_style/lib/src/line_splitting/solve_state.dart |
@@ -6,6 +6,7 @@ library dart_style.src.line_splitting.solve_state; |
import '../debug.dart' as debug; |
import '../rule/rule.dart'; |
+import '../whitespace.dart'; |
import 'line_splitter.dart'; |
import 'rule_set.dart'; |
@@ -25,11 +26,21 @@ class SolveState { |
final LineSplitter _splitter; |
final RuleSet _ruleValues; |
+ /// The set of [Rule]s that are bound by [_ruleValues]. |
+ /// |
+ /// Cached by [_ensureConstraints] for use by [_ensureUnboundConstraints]. |
+ Set<Rule> _boundRules; |
+ |
+ /// The set of [Rule]s that are not bound by [_ruleValues]. |
+ /// |
+ /// Cached by [_ensureConstraints] for use by [_ensureUnboundConstraints]. |
+ Set<Rule> _unboundRules; |
+ |
/// The unbound rules in this state that can be bound to produce new more |
/// refined states. |
/// |
/// Keeping this set small is the key to make the entire line splitter |
- /// perform well. If we consider too make rules at each state, our |
+ /// perform well. If we consider too many rules at each state, our |
/// exploration of the solution space is too branchy and we waste time on |
/// dead end solutions. |
/// |
@@ -75,6 +86,19 @@ class SolveState { |
/// unbound rules. |
Map<Rule, int> _constraints; |
+ /// The unbound rule values that are disallowed because they would place |
+ /// invalid constraints on the currently bound values. |
+ /// |
+ /// For example, say rule A is bound to 1 and B is unbound. If B has value |
+ /// 1, it constrains A to be 1. Likewise, if B is 2, it constrains A to be |
+ /// 2 as well. Since we already know A is 1, that means we know B cannot be |
+ /// bound to value 2. This means B is more limited in this state than it |
+ /// might be in another state that binds A to a different value. |
+ /// |
+ /// It's important to track this, because we can't allow to states to overlap |
+ /// if one permits more values for some unbound rule than the other does. |
+ Map<Rule, Set<int>> _unboundConstraints; |
+ |
/// The bound rules that appear inside lines also containing unbound rules. |
/// |
/// By appearing in the same line, it means these bound rules may affect the |
@@ -87,13 +111,9 @@ class SolveState { |
_calculateCost(); |
} |
- /// Gets the value to use for [rule], either the bound value or `0` if it |
- /// isn't bound. |
- int getValue(Rule rule) { |
- if (rule is HardSplitRule) return 0; |
- |
- return _ruleValues.getValue(rule); |
- } |
+ /// Gets the value to use for [rule], either the bound value or |
+ /// [Rule.unsplit] if it isn't bound. |
+ int getValue(Rule rule) => _ruleValues.getValue(rule); |
/// Returns `true` if this state is a better solution to use as the final |
/// result than [other]. |
@@ -212,11 +232,10 @@ class SolveState { |
/// Returns `true` if [other] overlaps this state. |
bool _isOverlapping(SolveState other) { |
- _ensureOverlapFields(); |
- other._ensureOverlapFields(); |
- |
// Lines that contain both bound and unbound rules must have the same |
// bound values. |
+ _ensureBoundRulesInUnboundLines(); |
+ other._ensureBoundRulesInUnboundLines(); |
if (_boundRulesInUnboundLines.length != |
other._boundRulesInUnboundLines.length) { |
return false; |
@@ -229,12 +248,30 @@ class SolveState { |
} |
} |
+ _ensureConstraints(); |
+ other._ensureConstraints(); |
if (_constraints.length != other._constraints.length) return false; |
for (var rule in _constraints.keys) { |
if (_constraints[rule] != other._constraints[rule]) return false; |
} |
+ _ensureUnboundConstraints(); |
+ other._ensureUnboundConstraints(); |
+ if (_unboundConstraints.length != other._unboundConstraints.length) { |
+ return false; |
+ } |
+ |
+ for (var rule in _unboundConstraints.keys) { |
+ var disallowed = _unboundConstraints[rule]; |
+ var otherDisallowed = other._unboundConstraints[rule]; |
+ |
+ if (disallowed.length != otherDisallowed.length) return false; |
+ for (var value in disallowed) { |
+ if (!otherDisallowed.contains(value)) return false; |
+ } |
+ } |
+ |
return true; |
} |
@@ -267,6 +304,8 @@ class SolveState { |
// And any expression nesting. |
indent += chunk.nesting.totalUsedIndent; |
+ |
+ if (chunk.indentBlock(getValue)) indent += Indent.expression; |
} |
_splits.add(i, indent); |
@@ -288,7 +327,8 @@ class SolveState { |
// The unbound rules in use by the current line. This will be null after |
// the first long line has completed. |
- var currentLineRules = []; |
+ var foundOverflowRules = false; |
+ var start = 0; |
endLine(int end) { |
// Track lines that went over the length. It is only rules contained in |
@@ -298,16 +338,16 @@ class SolveState { |
// Only try rules that are in the first long line, since we know at |
// least one of them *will* be split. |
- if (currentLineRules != null && currentLineRules.isNotEmpty) { |
- _liveRules.addAll(currentLineRules); |
- currentLineRules = null; |
- } |
- } else { |
- // The line fit, so don't keep track of its rules. |
- if (currentLineRules != null) { |
- currentLineRules.clear(); |
+ if (!foundOverflowRules) { |
+ for (var i = start; i < end; i++) { |
+ if (_addLiveRules(_splitter.chunks[i].rule)) { |
+ foundOverflowRules = true; |
+ } |
+ } |
} |
} |
+ |
+ start = end; |
} |
// The set of spans that contain chunks that ended up splitting. We store |
@@ -315,6 +355,9 @@ class SolveState { |
// one split occurs in it. |
var splitSpans = new Set(); |
+ // The nesting level of the chunk that ended the previous line. |
+ var previousNesting; |
+ |
for (var i = 0; i < _splitter.chunks.length; i++) { |
var chunk = _splitter.chunks[i]; |
@@ -329,11 +372,37 @@ class SolveState { |
splitSpans.addAll(chunk.spans); |
// Include the cost of the nested block. |
- if (chunk.blockChunks.isNotEmpty) { |
+ if (chunk.isBlock) { |
cost += |
_splitter.writer.formatBlock(chunk, _splits.getColumn(i)).cost; |
} |
+ // Do not allow sequential lines to have the same indentation but for |
+ // different reasons. In other words, don't allow different expressions |
+ // to claim the same nesting level on subsequent lines. |
+ // |
+ // A contrived example would be: |
+ // |
+ // function(inner( |
+ // argument), second( |
+ // another); |
+ // |
+ // For the most part, we prevent this by the constraints on splits. |
+ // For example, the above can't happen because the split before |
+ // "argument", forces the split before "second". |
+ // |
+ // But there are a couple of squirrely cases where it's hard to prevent |
+ // by construction. Instead, this outlaws it by penalizing it very |
+ // heavily if it happens to get this far. |
+ if (previousNesting != null && |
+ chunk.nesting.totalUsedIndent != 0 && |
+ chunk.nesting.totalUsedIndent == previousNesting.totalUsedIndent && |
+ !identical(chunk.nesting, previousNesting)) { |
+ _overflowChars += 10000; |
+ } |
+ |
+ previousNesting = chunk.nesting; |
+ |
// Start the new line. |
length = _splits.getColumn(i); |
} else { |
@@ -341,23 +410,12 @@ class SolveState { |
// Include the nested block inline, if any. |
length += chunk.unsplitBlockLength; |
- |
- // If we might be in the first overly long line, keep track of any |
- // unbound rules we encounter. These are ones that we'll want to try to |
- // bind to shorten the long line. |
- if (currentLineRules != null && |
- chunk.rule != null && |
- !chunk.isHardSplit && |
- !_ruleValues.contains(chunk.rule)) { |
- currentLineRules.add(chunk.rule); |
- } |
} |
} |
- // Add the costs for the rules that split. |
+ // Add the costs for the rules that have any splits. |
_ruleValues.forEach(_splitter.rules, (rule, value) { |
- // A rule may be bound to zero if another rule constrains it to not split. |
- if (value != 0) cost += rule.cost; |
+ if (value != Rule.unsplit) cost += rule.cost; |
}); |
// Add the costs for the spans containing splits. |
@@ -369,46 +427,32 @@ class SolveState { |
_splits.setCost(cost); |
} |
- /// Lazily initializes the fields needed to compare two states for overlap. |
+ /// Adds [rule] and all of the rules it constrains to the set of [_liveRules]. |
/// |
- /// We do this lazily because the calculation is a bit slow, and is only |
- /// needed when we have two states with the same score. |
- void _ensureOverlapFields() { |
- if (_constraints != null) return; |
- |
- _calculateConstraints(); |
- _calculateBoundRulesInUnboundLines(); |
- } |
+ /// Only does this if [rule] is a valid soft rule. Returns `true` if any new |
+ /// live rules were added. |
+ bool _addLiveRules(Rule rule) { |
+ if (rule == null) return false; |
- /// Initializes [_constraints] with any constraints the bound rules place on |
- /// the unbound rules. |
- void _calculateConstraints() { |
- _constraints = {}; |
- |
- var unboundRules = []; |
- var boundRules = []; |
+ var added = false; |
+ for (var constrained in rule.allConstrainedRules) { |
+ if (_ruleValues.contains(constrained)) continue; |
- for (var rule in _splitter.rules) { |
- if (_ruleValues.contains(rule)) { |
- boundRules.add(rule); |
- } else { |
- unboundRules.add(rule); |
- } |
+ _liveRules.add(constrained); |
+ added = true; |
} |
- for (var bound in boundRules) { |
- var value = _ruleValues.getValue(bound); |
- |
- for (var unbound in unboundRules) { |
- var constraint = bound.constrain(value, unbound); |
- if (constraint != null) { |
- _constraints[unbound] = constraint; |
- } |
- } |
- } |
+ return added; |
} |
- void _calculateBoundRulesInUnboundLines() { |
+ /// Lazily initializes the [_boundInUnboundLines], which is needed to compare |
+ /// two states for overlap. |
+ /// |
+ /// We do this lazily because the calculation is a bit slow, and is only |
+ /// needed when we have two states with the same score. |
+ void _ensureBoundRulesInUnboundLines() { |
+ if (_boundRulesInUnboundLines != null) return; |
+ |
_boundRulesInUnboundLines = new Set(); |
var boundInLine = new Set(); |
@@ -423,7 +467,7 @@ class SolveState { |
} |
var rule = _splitter.chunks[i].rule; |
- if (rule != null && rule is! HardSplitRule) { |
+ if (rule != null) { |
if (_ruleValues.contains(rule)) { |
boundInLine.add(rule); |
} else { |
@@ -435,6 +479,89 @@ class SolveState { |
if (hasUnbound) _boundRulesInUnboundLines.addAll(boundInLine); |
} |
+ /// Lazily initializes the [_constraints], which is needed to compare two |
+ /// states for overlap. |
+ /// |
+ /// We do this lazily because the calculation is a bit slow, and is only |
+ /// needed when we have two states with the same score. |
+ void _ensureConstraints() { |
+ if (_constraints != null) return; |
+ |
+ _unboundRules = new Set(); |
+ _boundRules = new Set(); |
+ |
+ for (var rule in _splitter.rules) { |
+ if (_ruleValues.contains(rule)) { |
+ _boundRules.add(rule); |
+ } else { |
+ _unboundRules.add(rule); |
+ } |
+ } |
+ |
+ _constraints = {}; |
+ |
+ for (var bound in _boundRules) { |
+ for (var unbound in bound.constrainedRules) { |
+ if (!_unboundRules.contains(unbound)) continue; |
+ |
+ var value = _ruleValues.getValue(bound); |
+ var constraint = bound.constrain(value, unbound); |
+ if (constraint != null) { |
+ _constraints[unbound] = constraint; |
+ } |
+ } |
+ } |
+ } |
+ |
+ /// Lazily initializes the [_unboundConstraints], which is needed to compare |
+ /// two states for overlap. |
+ /// |
+ /// We do this lazily because the calculation is a bit slow, and is only |
+ /// needed when we have two states with the same score. |
+ void _ensureUnboundConstraints() { |
+ if (_unboundConstraints != null) return; |
+ |
+ // _ensureConstraints should be called first which initializes these. |
+ assert(_boundRules != null); |
+ assert(_unboundRules != null); |
+ |
+ _unboundConstraints = {}; |
+ |
+ for (var unbound in _unboundRules) { |
+ var disallowedValues; |
+ |
+ for (var bound in unbound.constrainedRules) { |
+ if (!_boundRules.contains(bound)) continue; |
+ |
+ var boundValue = _ruleValues.getValue(bound); |
+ |
+ for (var value = 0; value < unbound.numValues; value++) { |
+ var constraint = unbound.constrain(value, bound); |
+ |
+ // If the unbound rule doesn't place any constraint on this bound |
+ // rule, we're fine. |
+ if (constraint == null) continue; |
+ |
+ // If the bound rule's value already meets the constraint it applies, |
+ // we don't need to track it. This way, two states that have the |
+ // same bound value, one of which has a satisfied constraint, are |
+ // still allowed to overlap. |
+ if (constraint == boundValue) continue; |
+ if (constraint == Rule.mustSplit && boundValue != Rule.unsplit) { |
+ continue; |
+ } |
+ |
+ if (disallowedValues == null) { |
+ disallowedValues = new Set(); |
+ _unboundConstraints[unbound] = disallowedValues; |
+ } |
+ |
+ disallowedValues.add(value); |
+ } |
+ } |
+ } |
+ } |
+ |
String toString() { |
var buffer = new StringBuffer(); |