prefer-moving-to-variable

Same as prefer-moving-to-variable in Dart Code Metrics

Path
lint/complexity/prefer-moving-to-variable
Default
Off, opt in
Origin
Dart Code Metrics
Rule
386 of 512

Flags repeated property/getter chains in local variable initializers.

Chains rooted in an identifier, this, or super, including receiver-based method calls, are candidates. Receiverless calls, constructors and fresh collections retain their separate evaluations. Initializer source text is compared within each statement list. Mutation and deferred late reads reset counts so a shared variable does not retain a stale value. Unresolved Type.member calls remain candidates because constructors and static methods are ambiguous.

Options

allowed-duplicated-chains (integer, default: 2) — the occurrence index at which a repeat is flagged; 2 flags the 2nd and later occurrences, 3 the 3rd and later. Values below 2 are clamped to 2.

Invalid

example.dartdart
// Test cases for prefer-moving-to-variable rule
// Repeated property/getter chains can share a local binding.

void duplicateGetterChain() {
  final a = service.configuration.value;
  final b = service.configuration.value;
}

void duplicateFieldAccess() {
  final x = obj.property.value;
  final y = obj.property.value;
}

class Processor {
  void process() {
    final first = this.data.value;
    final second = this.data.value;
    combine(first, second);
  }
}

void duplicateNullAwareChain() {
  final x = obj?.property?.value;
  final y = obj?.property?.value;
}

void duplicateAcrossReadOnlyDeclaration() {
  final v1 = obj.property.value;
  final unrelated = 42;
  final v2 = obj.property.value;
}

void duplicateAcrossPlainString() {
  final before = obj.property.value;
  final separator = ', ';
  final after = obj.property.value;
}

void duplicateMethodChain() {
  final chain1 = obj.method().chain().result;
  final chain2 = obj.method().chain().result;
}

void duplicateReceiverInvocation() {
  final filtered1 = list.where((e) => e > 5).toList();
  final filtered2 = list.where((e) => e > 5).toList();
}

void duplicateThemeLookup() {
  final first = Theme.of(context).textTheme;
  final second = Theme.of(context).textTheme;
}

void duplicateReceiverCall() {
  final first = service.nextValue();
  final second = service.nextValue();
}

void unrelatedNonLateName() {
  final shared = 1;
  final first = o.s;
  final unrelated = shared;
  final second = o.s;
}

void pureLateRead() {
  late final m = 1;
  final first = o.s;
  final unrelated = m;
  final second = o.s;
}

class StaticLookup {
  static Object of(Object context) => context;
}

void duplicateKnownStaticMethod(Object context) {
  final first = StaticLookup.of(context);
  final second = StaticLookup.of(context);
}

void capturedLateShadowedByParameter() {
  late final m = mutate();
  void inner(int m) {
    final a = o.s;
    final b = m;
    final c = o.s;
  }
  inner(1);
}

void capturedLateShadowedByLocal() {
  late final m = mutate();
  final inner = () {
    final m = 1;
    final a = o.s;
    final b = m;
    final c = o.s;
  };
  inner();
}

void creatingClosureDoesNotReadLate() {
  late final m = mutate();
  final a = o.s;
  final inner = () => m;
  final c = o.s;
}

void lateSwitchCaseScopes(int value) {
  switch (value) {
    case 0:
      late final m = mutate();
      final a = o.s;
      final b = m;
      final c = o.s;
      break;
    default:
      final m = 1;
      final a = o.s;
      final b = m;
      final c = o.s;
  }
}

void lateTryCatchFinallyScopes() {
  try {
    late final m = mutate();
    final a = o.s;
    final b = m;
    final c = o.s;
  } catch (error) {
    final m = 1;
    final a = o.s;
    final b = m;
    final c = o.s;
  } finally {
    final m = 1;
    final a = o.s;
    final b = m;
    final c = o.s;
  }
}
Extract this duplicate expression to a shared variable.
5 final a = service.configuration.value;
6 final b = service.configuration.value;
Extract this duplicate expression to a shared variable.
10 final x = obj.property.value;
11 final y = obj.property.value;
Extract this duplicate expression to a shared variable.
16 final first = this.data.value;
17 final second = this.data.value;
Extract this duplicate expression to a shared variable.
23 final x = obj?.property?.value;
24 final y = obj?.property?.value;
Extract this duplicate expression to a shared variable.
29 final unrelated = 42;
30 final v2 = obj.property.value;
Extract this duplicate expression to a shared variable.
35 final separator = ', ';
36 final after = obj.property.value;
Extract this duplicate expression to a shared variable.
40 final chain1 = obj.method().chain().result;
41 final chain2 = obj.method().chain().result;
Extract this duplicate expression to a shared variable.
45 final filtered1 = list.where((e) => e > 5).toList();
46 final filtered2 = list.where((e) => e > 5).toList();
Extract this duplicate expression to a shared variable.
50 final first = Theme.of(context).textTheme;
51 final second = Theme.of(context).textTheme;
Extract this duplicate expression to a shared variable.
55 final first = service.nextValue();
56 final second = service.nextValue();
Extract this duplicate expression to a shared variable.
62 final unrelated = shared;
63 final second = o.s;
Extract this duplicate expression to a shared variable.
69 final unrelated = m;
70 final second = o.s;
Extract this duplicate expression to a shared variable.
78 final first = StaticLookup.of(context);
79 final second = StaticLookup.of(context);
Extract this duplicate expression to a shared variable.
86 final b = m;
87 final c = o.s;
Extract this duplicate expression to a shared variable.
97 final b = m;
98 final c = o.s;
Extract this duplicate expression to a shared variable.
106 final inner = () => m;
107 final c = o.s;
Extract this duplicate expression to a shared variable.
121 final b = m;
122 final c = o.s;
Extract this duplicate expression to a shared variable.
135 final b = m;
136 final c = o.s;
Extract this duplicate expression to a shared variable.
140 final b = m;
141 final c = o.s;

Valid

example.dartdart
import 'dart:async';
import 'dart:core';
import 'dart:core' as core;

// Test cases for prefer-moving-to-variable rule
// No violations: unsupported initializers or chains separated by mutation barriers.

void distinctComputations() {
  final a = calculateExpensive(10);
  final b = calculateExpensive(20);
}

void singleUse() {
  final result = getResult(data);
  print(result);
}

// Trivial literals are not flagged
void trivialLiterals() {
  final x = 42;
  final y = 42;
  final s1 = "hello";
  final s2 = "hello";
}

class Processor {
  void process() {
    final first = getResult(dataA);
    final second = getResult(dataB);
    combine(first, second);
  }
}

// Only one assignment of complex expression
void singleComplexAssignment() {
  final x = obj.method().chain().result;
  print(x);
}

// Identifiers (trivial) don't trigger violation
void identifierDuplicates() {
  final a = variable1;
  final b = variable1;
}

// Different chained calls
void differentChains() {
  final c1 = obj.method1().result;
  final c2 = obj.method2().result;
}

void separateConstructors() {
  final first = Completer();
  final second = Completer();
  final namedFirst = Completer.sync();
  final namedSecond = Completer.sync();
}

void separateCalls() {
  final first = calculateExpensive(10);
  final second = calculateExpensive(10);
}

Future<void> separateAwaits() async {
  final first = await service.load();
  final second = await service.load();
}

void freshCollections() {
  final list1 = <int>[];
  final list2 = <int>[];
  final map1 = <String, int>{};
  final map2 = <String, int>{};
  final set1 = <int>{};
  final set2 = <int>{};
  final completers1 = [Completer()];
  final completers2 = [Completer()];
}

void separateOperators() {
  final first = a + b;
  final second = a + b;
}

void changingState() {
  final before = notifier.state;
  notifier.state = nextState;
  final after = notifier.state;
}

void changingThroughCall() {
  final before = LoggerService.instance;
  configureLogger();
  final after = LoggerService.instance;
}

void effectfulInitializer() {
  final before = notifier.state;
  final mutation = notifier.refresh();
  final after = notifier.state;
}

Future<void> stateAcrossAwait() async {
  final before = notifier.state;
  await refresh();
  final after = notifier.state;
}

void nestedMutation() {
  final before = notifier.state;
  if (condition) {
    notifier.state = nextState;
  }
  final after = notifier.state;
}

void mutatedInSameDeclaration() {
  final before = notifier.state,
      mutation = notifier.refresh(),
      after = notifier.state;
}

void reboundRoot() {
  final before = notifier.state;
  notifier = nextNotifier;
  final after = notifier.state;
}

final repeatedConstructorsInCallback = () {
  final first = Completer();
  final second = Completer();
};

Future<void> stateAcrossAwaitInitializer() async {
  final before = notifier.state;
  final loaded = await refresh();
  final after = notifier.state;
}

void deferredReads() {
  final before = notifier.state;
  late final after = notifier.state;
  notifier.state = nextState;
  use(before, after);
}

void separateDeferredReads() {
  late final before = notifier.state;
  late final after = notifier.state;
  use(before);
  notifier.state = nextState;
  use(after);
}

void deferredDeclarationBarrier() {
  final before = notifier.state;
  late final deferred = notifier.state;
  final after = notifier.state;
}

void interpolatedInitializerBarrier() {
  final before = notifier.state;
  final text = '${notifier.refresh()}';
  final after = notifier.state;
}

void lateReadAfterFirstChain() {
  late final m = mutate();
  final a = o.s;
  final b = m;
  final c = o.s;
}

void lateReadInsideExpression() {
  late final m = mutate();
  final a = o.s;
  final b = m.value;
  final c = o.s;
}

void outerDeferredRead() {
  late final m = mutate();
  if (condition) {
    final a = o.s;
    final b = m;
    final c = o.s;
  }
}

void freshControllerReceivers() {
  final first = TextEditingController().value;
  final second = TextEditingController().value;
  final explicitFirst = new TextEditingController().value;
  final explicitSecond = new TextEditingController().value;
}

class Fresh {
  Fresh.named();
  int get value => 0;
}

void resolvedFreshReceivers() {
  final a = Fresh.named().value;
  final b = Fresh.named().value;
  final c = DateTime.now().year;
  final d = DateTime.now().year;
  final e = core.DateTime(2025).year;
  final f = core.DateTime(2025).year;
  final g = StreamController.broadcast().stream;
  final h = StreamController.broadcast().stream;
  final i = List.filled(3, 0).length;
  final j = List.filled(3, 0).length;
}

void unrelatedLateName() {
  late final shared = mutate();
  print(shared);
}

void capturedLateInLocalFunction() {
  late final m = mutate();
  void inner() {
    final a = o.s;
    final b = m;
    final c = o.s;
  }
  inner();
}

void capturedLateInClosure() {
  late final m = mutate();
  final inner = () {
    final a = o.s;
    final b = m;
    final c = o.s;
  };
  inner();
}

void siblingTryBindingDoesNotShadowCapturedLate() {
  late final m = mutate();
  try {
    final m = 1;
    use(m);
  } catch (_) {
    final a = o.s;
    final b = m;
    final c = o.s;
  }
}

void siblingCaseBindingDoesNotShadowCapturedLate(int value) {
  late final m = mutate();
  switch (value) {
    case 0:
      final m = 1;
      use(m);
      break;
    default:
      final a = o.s;
      final b = m;
      final c = o.s;
  }
}

Configure prefer-moving-to-variable

Set its severity in falcon.json. See the configuration guide.

falcon.jsonjson
{
  "linter": {
    "rules": {
      "complexity": {
        "prefer-moving-to-variable": "error"
      }
    }
  }
}

Or silence one occurrence with a suppression comment; the reason is required.

example.dartdart
// falcon-ignore lint/complexity/prefer-moving-to-variable: <reason>
  • prefer-null-aware-operatorsprefer_null_aware_operatorsrecommended

    Flags x == null ? null : x.y (and the inverted x != null ? x.y : null).

  • unnecessary-constunnecessary_constrecommended

    Flags a redundant const keyword inside an already-const context.

  • unnecessary-getters-settersunnecessary_getters_settersrecommended

    Flags a trivial getter/setter pair that only reads and writes a private field.

  • unnecessary-lambdasunnecessary_lambdasrecommended

    Flags a closure that only forwards its parameters to a function a tear-off could name.

  • unnecessary-overridesunnecessary_overridesrecommended

    Flags a member that does nothing but forward to super with the same arguments and resolved contract.

  • widget-tree-depthflutter

    Limits the depth of an inline Flutter widget construction tree.

  • avoid-classes-with-only-static-membersavoid_classes_with_only_static_members

    Flags a class that declares only static members.

  • avoid-collapsible-if

    Flags an else-free if whose only then-statement is another else-free if.