// Copyright (c) 2020, 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. // The literal bool comparisons in this file are useful // for understanding the implementation and behavior of the example. // ignore_for_file: no_literal_bool_comparisons import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:scrape/scrape.dart'; void main(List arguments) { Scrape() ..addHistogram('Null-aware types') ..addHistogram('Null-aware chain lengths') // Boolean contexts where null-aware operators are used. ..addHistogram('Boolean contexts') // Expressions used to convert a null-aware expression to a // Boolean for use in Boolean context. ..addHistogram('Boolean conversions') ..addVisitor(NullVisitor.new) ..runCommandLine(arguments); } class NullVisitor extends ScrapeVisitor { @override void visitMethodInvocation(MethodInvocation node) { if (node.operator != null && node.operator!.type == TokenType.QUESTION_PERIOD) { _nullAware(node); } super.visitMethodInvocation(node); } @override void visitPropertyAccess(PropertyAccess node) { if (node.operator.type == TokenType.QUESTION_PERIOD) { _nullAware(node); } super.visitPropertyAccess(node); } void _nullAware(AstNode node) { var parent = node.parent; // Parentheses are purely syntactic. if (parent is ParenthesizedExpression) parent = parent.parent; // We want to treat a chain of null-aware operators as a single unit. We // use the top-most node (the last method in the chain) as the "real" one // because its parent is the context where the whole chain appears. if (parent is PropertyAccess && parent.operator.type == TokenType.QUESTION_PERIOD && parent.target == node || parent is MethodInvocation && parent.operator != null && parent.operator!.type == TokenType.QUESTION_PERIOD && parent.target == node) { // This node is not the root of a null-aware chain, so skip it. return; } // This node is the root of a null-aware chain. See how long the chain is. var length = 0; AstNode? chain = node; while (true) { if (chain is PropertyAccess && chain.operator.type == TokenType.QUESTION_PERIOD) { chain = chain.target; } else if (chain is MethodInvocation && chain.operator != null && chain.operator!.type == TokenType.QUESTION_PERIOD) { chain = chain.target; } else { break; } length++; } record('Null-aware chain lengths', length.toString()); void recordType(String label) { record('Null-aware types', label); } // See if the expression is an if condition. _checkCondition(node); if (parent is ExpressionStatement) { recordType("Expression statement 'foo?.bar();'"); return; } if (parent is ReturnStatement) { recordType("Return statement 'return foo?.bar();'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.BANG_EQ && parent.leftOperand == node && parent.rightOperand is BooleanLiteral && (parent.rightOperand as BooleanLiteral).value == true) { recordType("Compare to true 'foo?.bar() != true'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.EQ_EQ && parent.leftOperand == node && parent.rightOperand is BooleanLiteral && (parent.rightOperand as BooleanLiteral).value == true) { recordType("Compare to true 'foo?.bar() == true'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.BANG_EQ && parent.leftOperand == node && parent.rightOperand is BooleanLiteral && (parent.rightOperand as BooleanLiteral).value == false) { recordType("Compare to false 'foo?.bar() != false'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.EQ_EQ && parent.leftOperand == node && parent.rightOperand is BooleanLiteral && (parent.rightOperand as BooleanLiteral).value == false) { recordType("Compare to false 'foo?.bar() == false'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.BANG_EQ && parent.leftOperand == node && parent.rightOperand is NullLiteral) { recordType("Compare to null 'foo?.bar() != null'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.EQ_EQ && parent.leftOperand == node && parent.rightOperand is NullLiteral) { recordType("Compare to null 'foo?.bar() == null'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.EQ_EQ) { recordType("Compare to other expression 'foo?.bar() == bang'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.BANG_EQ) { recordType("Compare to other expression 'foo?.bar() != bang'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.QUESTION_QUESTION && parent.leftOperand == node) { recordType("Coalesce 'foo?.bar() ?? baz'"); return; } if (parent is BinaryExpression && parent.operator.type == TokenType.QUESTION_QUESTION && parent.rightOperand == node) { recordType("Reverse coalesce 'baz ?? foo?.bar()'"); return; } if (parent is ConditionalExpression && parent.condition == node) { recordType( "Condition in conditional expression 'foo?.bar() ? baz : bang", ); return; } if (parent is ConditionalExpression) { recordType("Then or else branch of conditional 'baz ? foo?.bar() : bang"); return; } if (parent is AsExpression && parent.expression == node) { recordType("Cast expression 'foo?.bar as Baz'"); return; } if (parent is AssignmentExpression && parent.leftHandSide == node) { recordType("Assign target 'foo?.bar ${parent.operator} baz'"); return; } if (parent is AssignmentExpression && parent.rightHandSide == node) { recordType("Assign value 'baz = foo?.bar()'"); return; } if (parent is VariableDeclaration && parent.initializer == node) { recordType("Variable initializer 'var baz = foo?.bar();'"); return; } if (parent is NamedArgument) { recordType("Named argument 'fn(name: foo?.bar())'"); return; } if (parent is ArgumentList && parent.arguments.contains(node)) { recordType("Positional argument 'fn(foo?.bar())'"); return; } if (parent is AwaitExpression) { recordType("Await 'await foo?.bar()'"); return; } if (parent is MapLiteralEntry || parent is ListLiteral) { recordType("Collection literal element '[foo?.bar()]'"); return; } if (parent is ExpressionFunctionBody) { recordType("Member body 'member() => foo?.bar();'"); return; } if (parent is InterpolationExpression) { recordType("String interpolation '\"blah \${foo?.bar()}\"'"); return; } if (parent is BinaryExpression) { recordType('Uncategorized $parent'); return; } recordType('Uncategorized ${parent.runtimeType}'); // Find the surrounding statement containing the null-aware. while (node is Expression) { node = node.parent!; } printNode(node); } void _checkCondition(AstNode node) { String? expression; // Look at the expression that immediately wraps the null-aware to see if // it deals with it somehow, like "foo?.bar ?? otherwise". var parent = node.parent; if (parent is ParenthesizedExpression) parent = parent.parent; if (parent is BinaryExpression && (parent.operator.type == TokenType.EQ_EQ || parent.operator.type == TokenType.BANG_EQ || parent.operator.type == TokenType.QUESTION_QUESTION) && (parent.rightOperand is NullLiteral || parent.rightOperand is BooleanLiteral)) { expression = 'foo?.bar ${parent.operator} ${parent.rightOperand}'; // This does handle it, so see the context where it appears. node = parent; if (node is ParenthesizedExpression) node = node.parent as Expression; parent = node.parent; if (parent is ParenthesizedExpression) parent = parent.parent; } String? context; if (parent is IfStatement && node == parent.expression) { context = 'if'; } else if (parent is BinaryExpression && parent.operator.type == TokenType.AMPERSAND_AMPERSAND) { context = '&&'; } else if (parent is BinaryExpression && parent.operator.type == TokenType.BAR_BAR) { context = '||'; } else if (parent is WhileStatement && node == parent.condition) { context = 'while'; } else if (parent is DoStatement && node == parent.condition) { context = 'do'; } else if (parent is ForStatement && parent.forLoopParts is ForParts && node == (parent.forLoopParts as ForParts).condition) { context = 'for'; } else if (parent is ConditionalExpression && node == parent.condition) { context = '?:'; } if (context != null) { record('Boolean contexts', context); if (expression != null) { record('Boolean conversions', expression); } else { record('Boolean conversions', 'unknown: $node'); } } } }