| // Copyright (c) 2018, 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 'package:analyzer/analysis_rule/rule_context.dart'; |
| import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/error/error.dart'; |
| |
| import '../analyzer.dart'; |
| import '../extensions.dart'; |
| |
| const _desc = r'Do not pass `null` as an argument where a closure is expected.'; |
| |
| List<NonNullableFunction> _constructorsWithNonNullableArguments = |
| <NonNullableFunction>[ |
| NonNullableFunction('dart.async', 'Future', null, positional: [0]), |
| NonNullableFunction('dart.async', 'Future', 'microtask', positional: [0]), |
| NonNullableFunction('dart.async', 'Future', 'sync', positional: [0]), |
| NonNullableFunction('dart.async', 'Timer', null, positional: [1]), |
| NonNullableFunction('dart.async', 'Timer', 'periodic', positional: [1]), |
| NonNullableFunction('dart.core', 'List', 'generate', positional: [1]), |
| ]; |
| |
| final Map<String, Set<NonNullableFunction>> |
| _instanceMethodsWithNonNullableArguments = { |
| 'any': { |
| NonNullableFunction('dart.core', 'Iterable', 'any', positional: [0]), |
| }, |
| 'complete': { |
| NonNullableFunction('dart.async', 'Future', 'complete', positional: [0]), |
| }, |
| 'every': { |
| NonNullableFunction('dart.core', 'Iterable', 'every', positional: [0]), |
| }, |
| 'expand': { |
| NonNullableFunction('dart.core', 'Iterable', 'expand', positional: [0]), |
| }, |
| 'firstWhere': { |
| NonNullableFunction( |
| 'dart.core', |
| 'Iterable', |
| 'firstWhere', |
| positional: [0], |
| named: ['orElse'], |
| ), |
| }, |
| 'forEach': { |
| NonNullableFunction('dart.core', 'Iterable', 'forEach', positional: [0]), |
| NonNullableFunction('dart.core', 'Map', 'forEach', positional: [0]), |
| }, |
| 'fold': { |
| NonNullableFunction('dart.core', 'Iterable', 'fold', positional: [1]), |
| }, |
| 'lastWhere': { |
| NonNullableFunction( |
| 'dart.core', |
| 'Iterable', |
| 'lastWhere', |
| positional: [0], |
| named: ['orElse'], |
| ), |
| }, |
| 'map': { |
| NonNullableFunction('dart.core', 'Iterable', 'map', positional: [0]), |
| }, |
| 'putIfAbsent': { |
| NonNullableFunction('dart.core', 'Map', 'putIfAbsent', positional: [1]), |
| }, |
| 'reduce': { |
| NonNullableFunction('dart.core', 'Iterable', 'reduce', positional: [0]), |
| }, |
| 'removeWhere': { |
| NonNullableFunction( |
| 'dart.collection', |
| 'Queue', |
| 'removeWhere', |
| positional: [0], |
| ), |
| NonNullableFunction('dart.core', 'List', 'removeWhere', positional: [0]), |
| NonNullableFunction('dart.core', 'Set', 'removeWhere', positional: [0]), |
| }, |
| 'replaceAllMapped': { |
| NonNullableFunction( |
| 'dart.core', |
| 'String', |
| 'replaceAllMapped', |
| positional: [1], |
| ), |
| }, |
| 'replaceFirstMapped': { |
| NonNullableFunction( |
| 'dart.core', |
| 'String', |
| 'replaceFirstMapped', |
| positional: [1], |
| ), |
| }, |
| 'retainWhere': { |
| NonNullableFunction( |
| 'dart.collection', |
| 'Queue', |
| 'retainWhere', |
| positional: [0], |
| ), |
| NonNullableFunction('dart.core', 'List', 'retainWhere', positional: [0]), |
| NonNullableFunction('dart.core', 'Set', 'retainWhere', positional: [0]), |
| }, |
| 'singleWhere': { |
| NonNullableFunction( |
| 'dart.core', |
| 'Iterable', |
| 'singleWhere', |
| positional: [0], |
| named: ['orElse'], |
| ), |
| }, |
| 'skipWhile': { |
| NonNullableFunction('dart.core', 'Iterable', 'skipWhile', positional: [0]), |
| }, |
| 'splitMapJoin': { |
| NonNullableFunction( |
| 'dart.core', |
| 'String', |
| 'splitMapJoin', |
| named: ['onMatch', 'onNonMatch'], |
| ), |
| }, |
| 'takeWhile': { |
| NonNullableFunction('dart.core', 'Iterable', 'takeWhile', positional: [0]), |
| }, |
| 'then': { |
| NonNullableFunction( |
| 'dart.async', |
| 'Future', |
| 'then', |
| positional: [0], |
| named: ['onError'], |
| ), |
| }, |
| 'where': { |
| NonNullableFunction('dart.core', 'Iterable', 'where', positional: [0]), |
| }, |
| }; |
| |
| List<NonNullableFunction> _staticFunctionsWithNonNullableArguments = |
| <NonNullableFunction>[ |
| NonNullableFunction( |
| 'dart.async', |
| null, |
| 'scheduleMicrotask', |
| positional: [0], |
| ), |
| NonNullableFunction('dart.async', 'Future', 'doWhile', positional: [0]), |
| NonNullableFunction('dart.async', 'Future', 'forEach', positional: [1]), |
| NonNullableFunction('dart.async', 'Future', 'wait', named: ['cleanUp']), |
| NonNullableFunction('dart.async', 'Timer', 'run', positional: [0]), |
| ]; |
| |
| /// Function with closure parameters that cannot accept null arguments. |
| class NonNullableFunction { |
| final String library; |
| final String? type; |
| final String? name; |
| final List<int> positional; |
| final List<String> named; |
| |
| NonNullableFunction( |
| this.library, |
| this.type, |
| this.name, { |
| this.positional = const <int>[], |
| this.named = const <String>[], |
| }); |
| |
| @override |
| int get hashCode => |
| Object.hash(library.hashCode, type.hashCode, name.hashCode); |
| |
| /// Two [NonNullableFunction] objects are equal if their [library], [type], |
| /// and [name] are equal, for the purpose of discovering whether a function |
| /// invocation is among a collection of non-nullable functions. |
| @override |
| bool operator ==(Object other) => |
| other is NonNullableFunction && other.hashCode == hashCode; |
| } |
| |
| class NullClosures extends LintRule { |
| NullClosures() : super(name: LintNames.null_closures, description: _desc); |
| |
| @override |
| DiagnosticCode get diagnosticCode => LinterLintCode.nullClosures; |
| |
| @override |
| void registerNodeProcessors( |
| RuleVisitorRegistry registry, |
| RuleContext context, |
| ) { |
| var visitor = _Visitor(this); |
| registry.addInstanceCreationExpression(this, visitor); |
| registry.addMethodInvocation(this, visitor); |
| } |
| } |
| |
| class _Visitor extends SimpleAstVisitor<void> { |
| final LintRule rule; |
| |
| _Visitor(this.rule); |
| |
| @override |
| void visitInstanceCreationExpression(InstanceCreationExpression node) { |
| var constructorName = node.constructorName; |
| var type = node.staticType; |
| for (var constructor in _constructorsWithNonNullableArguments) { |
| if (constructorName.name?.name == constructor.name) { |
| if (type.extendsClass(constructor.type, constructor.library)) { |
| _checkNullArgForClosure( |
| node.argumentList, |
| constructor.positional, |
| constructor.named, |
| ); |
| } |
| } |
| } |
| } |
| |
| @override |
| void visitMethodInvocation(MethodInvocation node) { |
| var target = node.target; |
| var methodName = node.methodName.name; |
| var element = target is Identifier ? target.element : null; |
| if (element is ClassElement) { |
| // Static function called, "target" is the class. |
| for (var function in _staticFunctionsWithNonNullableArguments) { |
| if (methodName == function.name) { |
| if (element.name == function.type) { |
| _checkNullArgForClosure( |
| node.argumentList, |
| function.positional, |
| function.named, |
| ); |
| } |
| } |
| } |
| } else { |
| // Instance method called, "target" is the instance. |
| var targetType = target?.staticType; |
| var method = _getInstanceMethod(targetType, methodName); |
| if (method == null) return; |
| |
| _checkNullArgForClosure( |
| node.argumentList, |
| method.positional, |
| method.named, |
| ); |
| } |
| } |
| |
| void _checkNullArgForClosure( |
| ArgumentList node, |
| List<int> positions, |
| List<String> names, |
| ) { |
| var args = node.arguments; |
| for (var i = 0; i < args.length; i++) { |
| var arg = args[i]; |
| |
| if (arg is NamedExpression) { |
| if (arg.expression is NullLiteral && |
| names.contains(arg.name.label.name)) { |
| rule.reportAtNode(arg); |
| } |
| } else { |
| if (arg is NullLiteral && positions.contains(i)) { |
| rule.reportAtNode(arg); |
| } |
| } |
| } |
| } |
| |
| NonNullableFunction? _getInstanceMethod(DartType? type, String methodName) { |
| var possibleMethods = _instanceMethodsWithNonNullableArguments[methodName]; |
| if (possibleMethods == null) return null; |
| |
| if (type is! InterfaceType) return null; |
| |
| NonNullableFunction? getMethod(String? libraryName, String className) { |
| if (libraryName == null) return null; |
| return possibleMethods.lookup( |
| NonNullableFunction(libraryName, className, methodName), |
| ); |
| } |
| |
| var element = type.element; |
| if (element.isSynthetic) return null; |
| |
| var elementName = element.name; |
| if (elementName == null) { |
| return null; |
| } |
| |
| var method = getMethod(element.library.name, elementName); |
| if (method != null) return method; |
| |
| for (var supertype in element.allSupertypes) { |
| var superElement = supertype.element; |
| if (superElement.name case var superElementName?) { |
| method = getMethod(superElement.library.name, superElementName); |
| if (method != null) return method; |
| } |
| } |
| return null; |
| } |
| } |