unnecessary-lambdas

Same as unnecessary_lambdas in Dart lints

Path
lint/complexity/unnecessary-lambdas
Default
On (recommended)
Origin
Dart lints
Rule
457 of 512
recommended

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

names.forEach((name) { print(name); }) allocates a closure whose whole job is to call print; names.forEach(print) says the same thing. The rule fires on an anonymous, synchronous closure whose body is a single invocation — => f(a, b), { f(a, b); }, or { return f(a, b); } — passing exactly the closure's positional parameters in order and named parameters under the same names, and on (a) => C(a) constructor calls a C.new tear-off could replace. Like the analyzer, this does not compare optional parameter defaults. The invoked function must stay the same object between calls:

  • a top-level, local, static, or instance method, or a final, non-late variable or field holding a function (the analyzer does not count a const variable, field, or enum value as final);
  • reached through a receiver that is a class name, an import prefix, or a final variable or field (including immutable record fields), never through this, a parameter of the closure, or a null-aware access (a?.b). A member falcon cannot resolve on a stable receiver is taken to be a method, as the analyzer treats any member that is not a getter.

A function or method is itself a stable receiver, including p.call(x). Optional constructor parameters do not prevent a tear-off from replacing a required-parameter closure when the constructor accepts those arguments. An optional closure parameter must stay optional on the target; legacy @required annotations do not change that shape. Constructor invocations with named arguments are excluded, matching the analyzer. Optional positional super formals retain their optional shape even when their inherited default value is unavailable.

Generic method invocations with explicit type arguments, calls through deferred imports, and const constructor calls are skipped. A constructor call only counts in an arrow body, and the constructor tear-off must fit the closure's type, so every closure parameter's type must be known (written or supplied by the context). When the closure is a named argument or a variable initializer, a method tear-off must also fit the closure's type: the parameter types must be known, and a block body that discards the call must be in a void-returning context (otherwise the closure returns Null).

falcon stays silent when it cannot resolve the invoked function or receiver. Constructor calls are only checked when constructor tear-offs are enabled (language version 2.15 or later); before that, (a) => C(a) is silent.

Invalid

example.dartdart
void log(Object? value) {}

class Point {
  Point(this.x);
  final int x;
}

class Logger {
  void write(Object? value) {}
  static void shared(Object? value) {}
}

class Screen {
  final Logger logger = Logger();
  final List<int> values = [];

  void show(List<String> names) {
    names.forEach((name) => logger.write(name));
    names.forEach((name) => describe(name));
  }

  void describe(String name) {}
}

void main(List<String> names, List<int> numbers) {
  names.forEach((name) {
    print(name);
  });
  names.forEach((name) => log(name));
  names.forEach((name) {
    return Logger.shared(name);
  });
  final logger = Logger();
  names.forEach((name) => logger.write(name));
  final points = numbers.map((x) => Point(x));
  final sink = <int>[];
  numbers.forEach((n) => sink.add(n));
  void local(int value) {}
  numbers.forEach((n) => local(n));
  print(points);
}

class Implicit {}

class Gen<T> {
  Gen();
}

class Queue2 {
  final List<int> items = [];
  final StringBuffer buffer = StringBuffer();

  void run(void Function() f) {}
  void named({void Function()? onTap}) {}

  void calls() {
    run(() {
      items.clear();
    });
    // A member falcon does not index on a stable receiver is a method.
    run(() => buffer.clear());
    // A block body discarding the call in a `void`-returning named context.
    named(onTap: () {
      items.clear();
    });
  }
}

void constructors() {
  final implicit = () => Implicit();
  final generic = () => Gen<int>();
  final identity = () => Set<int>.identity();
  print([implicit, generic, identity]);
}

// A `final` loop variable is stable even when its name is reused elsewhere.
void loops(List<Logger> loggers, List<String> names) {
  for (final logger in loggers) {
    names.forEach((name) => logger.write(name));
  }
}

void reused(List<String> names) {
  var logger = Logger();
  logger = Logger();
  names.forEach(logger.write);
}

// A list literal's element type is the closure's context.
List<Point Function(int)> builders() => [(x) => Point(x)];

void p(int value) {}

class OptionalConstructor {
  OptionalConstructor([String value = '']);
}

// Matches StringBuffer's optional Object constructor without loading SDK sources.
class Buffer {
  Buffer([Object content = '']);
}

void functionCallAndOptionalConstructors(List<int> numbers, List<String> names) {
  numbers.forEach((value) => log.call(value));
  numbers.forEach((value) => p.call(value));
  names.map((value) => Buffer(value));
  names.map((value) => Exception(value));
  names.map((value) => OptionalConstructor(value));
  names.map((String value) => OptionalConstructor(value));
}
Closure should be a tearoff.
17 void show(List<String> names) {
18 names.forEach((name) => logger.write(name));
Closure should be a tearoff.
18 names.forEach((name) => logger.write(name));
19 names.forEach((name) => describe(name));
Closure should be a tearoff.
25void main(List<String> names, List<int> numbers) {
26 names.forEach((name) {
Closure should be a tearoff.
28 });
29 names.forEach((name) => log(name));
Closure should be a tearoff.
29 names.forEach((name) => log(name));
30 names.forEach((name) {
Closure should be a tearoff.
33 final logger = Logger();
34 names.forEach((name) => logger.write(name));
Closure should be a tearoff.
34 names.forEach((name) => logger.write(name));
35 final points = numbers.map((x) => Point(x));
Closure should be a tearoff.
36 final sink = <int>[];
37 numbers.forEach((n) => sink.add(n));
Closure should be a tearoff.
38 void local(int value) {}
39 numbers.forEach((n) => local(n));
Closure should be a tearoff.
56 void calls() {
57 run(() {
Closure should be a tearoff.
60 // A member falcon does not index on a stable receiver is a method.
61 run(() => buffer.clear());
Closure should be a tearoff.
62 // A block body discarding the call in a `void`-returning named context.
63 named(onTap: () {
Closure should be a tearoff.
69void constructors() {
70 final implicit = () => Implicit();
Closure should be a tearoff.
70 final implicit = () => Implicit();
71 final generic = () => Gen<int>();
Closure should be a tearoff.
71 final generic = () => Gen<int>();
72 final identity = () => Set<int>.identity();
Closure should be a tearoff.
78 for (final logger in loggers) {
79 names.forEach((name) => logger.write(name));
Closure should be a tearoff.
89// A list literal's element type is the closure's context.
90List<Point Function(int)> builders() => [(x) => Point(x)];
Closure should be a tearoff.
103void functionCallAndOptionalConstructors(List<int> numbers, List<String> names) {
104 numbers.forEach((value) => log.call(value));
Closure should be a tearoff.
104 numbers.forEach((value) => log.call(value));
105 numbers.forEach((value) => p.call(value));
Closure should be a tearoff.
105 numbers.forEach((value) => p.call(value));
106 names.map((value) => Buffer(value));
Closure should be a tearoff.
106 names.map((value) => Buffer(value));
107 names.map((value) => Exception(value));
Closure should be a tearoff.
107 names.map((value) => Exception(value));
108 names.map((value) => OptionalConstructor(value));
Closure should be a tearoff.
108 names.map((value) => OptionalConstructor(value));
109 names.map((String value) => OptionalConstructor(value));

Valid

example.dartdart
import 'dart:convert';

void log(Object? value) {}

void pair(Object? a, Object? b) {}

class Logger {
  Logger();
  const Logger.constant();
  void write(Object? value) {}
}

class Screen {
  Logger logger = Logger();

  void show(List<String> names) {
    names.forEach((name) => logger.write(name));
    names.forEach((name) => this.write(name));
  }

  void write(String name) {}
}

Logger currentLogger() => Logger();

void main(List<String> names, List<List<String>> groups) async {
  names.forEach((name) => log(name.length));
  names.forEach((name) => pair(name, name));
  names.forEach((name) {
    log(name);
    log(name);
  });
  names.forEach((name) async => log(name));
  names.forEach((name) => currentLogger().write(name));
  var logger = Logger();
  names.forEach((name) => logger.write(name));
  Logger? maybe = Logger();
  names.forEach((name) => maybe?.write(name));
  groups.forEach((group) => group.add('x'));
  final callback = (name) => logger.write(name);
  names.forEach(([Object? name]) => log(name));
  print(callback);
}

class Box {
  Box(this.name);
  final String name;
  static const Logger shared = Logger.constant();
}

enum Level {
  info;

  void write(Object? value) {}
}

const topLogger = Logger.constant();

class Recorder {
  List<int> states = [];

  Recorder(Stream<int> stream) {
    // A non-final field is not a stable receiver.
    stream.listen((state) {
      states.add(state);
    });
  }
}

void contexts(List<String> names) {
  // The closure's own type returns `Null`, which `log`'s tear-off does not fit.
  final f = (String name) {
    log(name);
  };
  // The analyzer does not count `const` variables and fields as final.
  names.forEach((name) => Box.shared.write(name));
  names.forEach((name) => topLogger.write(name));
  const local = Logger.constant();
  names.forEach((name) => local.write(name));
  names.forEach((name) => Level.info.write(name));
  // A block body is not checked for constructor calls.
  names.map((name) {
    return Box(name);
  });
  // A constructor tear-off must fit the closure's (dynamic) parameter type.
  final g = (name) => Box(name);
  print([f, g]);
}

class Lazy {
  late final Logger logger = Logger();

  // A `late` field is not stable.
  void show(List<String> names) {
    names.forEach((name) => logger.write(name));
  }
}

// Imported SDK const values are not stable final receivers.
Future<dynamic> decodeInput(Future<String> input) =>
    input.then((response) => json.decode(response));

Configure unnecessary-lambdas

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

falcon.jsonjson
{
  "linter": {
    "rules": {
      "complexity": {
        "unnecessary-lambdas": "error"
      }
    }
  }
}

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

example.dartdart
// falcon-ignore lint/complexity/unnecessary-lambdas: <reason>