Jamil Saadeh f26eddba00
Null aware elements clean-ups (#173074)
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->
Converted more null checks to null aware elements.
The analyzer had many false negatives and the regex became pretty wild
to find many of them 😂

I've found more in the engine folder but I'll apply the changes in a
separate PR

Part of #172188 

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-08-13 15:36:14 +00:00

561 lines
16 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of 'reporting.dart';
const _kFlutterUA = 'UA-67589403-6';
abstract class Usage {
/// Create a new Usage instance; [versionOverride], [configDirOverride], and
/// [logFile] are used for testing.
factory Usage({
String settingsName = 'flutter',
String? versionOverride,
String? configDirOverride,
String? logFile,
AnalyticsFactory? analyticsIOFactory,
FirstRunMessenger? firstRunMessenger,
required bool runningOnBot,
}) => _DefaultUsage.initialize(
settingsName: settingsName,
versionOverride: versionOverride,
configDirOverride: configDirOverride,
logFile: logFile,
analyticsIOFactory: analyticsIOFactory,
runningOnBot: runningOnBot,
firstRunMessenger: firstRunMessenger,
);
/// Uses the global [Usage] instance to send a 'command' to analytics.
static void command(String command, {CustomDimensions? parameters}) =>
globals.flutterUsage.sendCommand(command, parameters: parameters);
/// Whether analytics reporting should be suppressed.
bool get suppressAnalytics;
/// Suppress analytics for this session.
set suppressAnalytics(bool value);
/// Whether analytics reporting is enabled.
bool get enabled;
/// Enable or disable reporting analytics.
set enabled(bool value);
/// A stable randomly generated UUID used to deduplicate multiple identical
/// reports coming from the same computer.
String get clientId;
/// Sends a 'command' to the underlying analytics implementation.
///
/// Using [command] above is preferred to ensure that the parameter
/// keys are well-defined in [CustomDimensions] above.
void sendCommand(String command, {CustomDimensions? parameters});
/// Sends an 'event' to the underlying analytics implementation.
///
/// This method should not be used directly, instead see the
/// event types defined in this directory in `events.dart`.
@visibleForOverriding
@visibleForTesting
void sendEvent(
String category,
String parameter, {
String? label,
int? value,
CustomDimensions? parameters,
});
/// Sends timing information to the underlying analytics implementation.
void sendTiming(String category, String variableName, Duration duration, {String? label});
/// Sends an exception to the underlying analytics implementation.
void sendException(dynamic exception);
/// Fires whenever analytics data is sent over the network.
@visibleForTesting
Stream<Map<String, dynamic>> get onSend;
/// Returns when the last analytics event has been sent, or after a fixed
/// (short) delay, whichever is less.
Future<void> ensureAnalyticsSent();
/// Prints a welcome message that informs the tool user about the collection
/// of anonymous usage information.
void printWelcome();
}
typedef AnalyticsFactory =
Analytics Function(
String trackingId,
String applicationName,
String applicationVersion, {
String analyticsUrl,
Directory? documentDirectory,
});
Analytics _defaultAnalyticsIOFactory(
String trackingId,
String applicationName,
String applicationVersion, {
String? analyticsUrl,
Directory? documentDirectory,
}) {
return AnalyticsIO(
trackingId,
applicationName,
applicationVersion,
analyticsUrl: analyticsUrl,
documentDirectory: documentDirectory,
);
}
class _DefaultUsage implements Usage {
_DefaultUsage._({
required bool suppressAnalytics,
required Analytics analytics,
required this.firstRunMessenger,
required SystemClock clock,
}) : _suppressAnalytics = suppressAnalytics,
_analytics = analytics,
_clock = clock;
static _DefaultUsage initialize({
String settingsName = 'flutter',
String? versionOverride,
String? configDirOverride,
String? logFile,
AnalyticsFactory? analyticsIOFactory,
required FirstRunMessenger? firstRunMessenger,
required bool runningOnBot,
}) {
final FlutterVersion flutterVersion = globals.flutterVersion;
final String version =
versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
final suppressEnvFlag = globals.platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true';
final String? logFilePath =
logFile ?? globals.platform.environment['FLUTTER_ANALYTICS_LOG_FILE'];
final bool usingLogFile = logFilePath != null && logFilePath.isNotEmpty;
final AnalyticsFactory analyticsFactory = analyticsIOFactory ?? _defaultAnalyticsIOFactory;
var suppressAnalytics = false;
var skipAnalyticsSessionSetup = false;
Analytics? setupAnalytics;
if ( // To support testing, only allow other signals to suppress analytics
// when analytics are not being shunted to a file.
!usingLogFile &&
( // Ignore local user branches.
version.startsWith('[user-branch]') ||
// Many CI systems don't do a full git checkout.
version.endsWith('/unknown') ||
// Ignore bots.
runningOnBot ||
// Ignore when suppressed by FLUTTER_SUPPRESS_ANALYTICS.
suppressEnvFlag)) {
// If we think we're running on a CI system, suppress sending analytics.
suppressAnalytics = true;
setupAnalytics = AnalyticsMock();
skipAnalyticsSessionSetup = true;
}
if (usingLogFile) {
setupAnalytics ??= LogToFileAnalytics(logFilePath);
} else {
try {
ErrorHandlingFileSystem.noExitOnFailure(() {
setupAnalytics = analyticsFactory(
_kFlutterUA,
settingsName,
version,
documentDirectory: configDirOverride != null
? globals.fs.directory(configDirOverride)
: null,
);
});
} on Exception catch (e) {
globals.printTrace('Failed to initialize analytics reporting: $e');
suppressAnalytics = true;
setupAnalytics ??= AnalyticsMock();
skipAnalyticsSessionSetup = true;
}
}
final Analytics analytics = setupAnalytics!;
if (!skipAnalyticsSessionSetup) {
// Report a more detailed OS version string than package:usage does by default.
analytics.setSessionValue(CustomDimensionsEnum.sessionHostOsDetails.cdKey, globals.os.name);
// Send the branch name as the "channel".
analytics.setSessionValue(
CustomDimensionsEnum.sessionChannelName.cdKey,
flutterVersion.getBranchName(redactUnknownBranches: true),
);
// For each flutter experimental feature, record a session value in a comma
// separated list.
final String enabledFeatures = featureFlags.allFeatures
.where((Feature feature) {
final String? configSetting = feature.configSetting;
return configSetting != null && globals.config.getValue(configSetting) == true;
})
.map((Feature feature) => feature.configSetting)
.join(',');
analytics.setSessionValue(CustomDimensionsEnum.enabledFlutterFeatures.cdKey, enabledFeatures);
// Record the host as the application installer ID - the context that flutter_tools is running in.
if (globals.platform.environment.containsKey('FLUTTER_HOST')) {
analytics.setSessionValue('aiid', globals.platform.environment['FLUTTER_HOST']);
}
analytics.analyticsOpt = AnalyticsOpt.optOut;
}
return _DefaultUsage._(
suppressAnalytics: suppressAnalytics,
analytics: analytics,
firstRunMessenger: firstRunMessenger,
clock: globals.systemClock,
);
}
final Analytics _analytics;
final FirstRunMessenger? firstRunMessenger;
var _printedWelcome = false;
var _suppressAnalytics = false;
final SystemClock _clock;
@override
bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun;
@override
set suppressAnalytics(bool value) {
_suppressAnalytics = value;
}
@override
bool get enabled => _analytics.enabled;
@override
set enabled(bool value) {
_analytics.enabled = value;
}
@override
String get clientId => _analytics.clientId;
@override
void sendCommand(String command, {CustomDimensions? parameters}) {
if (suppressAnalytics) {
return;
}
_analytics.sendScreenView(
command,
parameters: CustomDimensions(
localTime: formatDateTime(_clock.now()),
).merge(parameters).toMap(),
);
}
@override
void sendEvent(
String category,
String parameter, {
String? label,
int? value,
CustomDimensions? parameters,
}) {
if (suppressAnalytics) {
return;
}
_analytics.sendEvent(
category,
parameter,
label: label,
value: value,
parameters: CustomDimensions(
localTime: formatDateTime(_clock.now()),
).merge(parameters).toMap(),
);
}
@override
void sendTiming(String category, String variableName, Duration duration, {String? label}) {
if (suppressAnalytics) {
return;
}
_analytics.sendTiming(variableName, duration.inMilliseconds, category: category, label: label);
}
@override
void sendException(dynamic exception) {
if (suppressAnalytics) {
return;
}
_analytics.sendException(exception.runtimeType.toString());
}
@override
Stream<Map<String, dynamic>> get onSend => _analytics.onSend;
@override
Future<void> ensureAnalyticsSent() async {
// TODO(devoncarew): This may delay tool exit and could cause some analytics
// events to not be reported. Perhaps we could send the analytics pings
// out-of-process from flutter_tools?
await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
}
@override
void printWelcome() {
// Only print once per run.
if (_printedWelcome) {
return;
}
// Display the welcome message if this is the first run of the tool or if
// the license terms have changed since it was last displayed.
final FirstRunMessenger? messenger = firstRunMessenger;
if (messenger != null && messenger.shouldDisplayLicenseTerms()) {
globals.printStatus('');
globals.printStatus(messenger.licenseTerms, emphasis: true);
_printedWelcome = true;
messenger.confirmLicenseTermsDisplayed();
}
}
}
// An Analytics mock that logs to file. Unimplemented methods goes to stdout.
// But stdout can't be used for testing since wrapper scripts like
// xcode_backend.sh etc manipulates them.
class LogToFileAnalytics extends AnalyticsMock {
LogToFileAnalytics(String logFilePath)
: logFile = globals.fs.file(logFilePath)..createSync(recursive: true),
super(true);
final File logFile;
final _sessionValues = <String, String>{};
final _sendController = StreamController<Map<String, dynamic>>.broadcast(sync: true);
@override
Stream<Map<String, dynamic>> get onSend => _sendController.stream;
@override
Future<void> sendScreenView(String viewName, {Map<String, String>? parameters}) {
if (!enabled) {
return Future<void>.value();
}
parameters ??= <String, String>{};
parameters['viewName'] = viewName;
parameters.addAll(_sessionValues);
_sendController.add(parameters);
logFile.writeAsStringSync('screenView $parameters\n', mode: FileMode.append);
return Future<void>.value();
}
@override
Future<void> sendEvent(
String category,
String action, {
String? label,
int? value,
Map<String, String>? parameters,
}) {
if (!enabled) {
return Future<void>.value();
}
parameters ??= <String, String>{};
parameters['category'] = category;
parameters['action'] = action;
_sendController.add(parameters);
logFile.writeAsStringSync('event $parameters\n', mode: FileMode.append);
return Future<void>.value();
}
@override
Future<void> sendTiming(String variableName, int time, {String? category, String? label}) {
if (!enabled) {
return Future<void>.value();
}
final parameters = <String, String>{
'variableName': variableName,
'time': '$time',
'category': ?category,
'label': ?label,
};
_sendController.add(parameters);
logFile.writeAsStringSync('timing $parameters\n', mode: FileMode.append);
return Future<void>.value();
}
@override
void setSessionValue(String param, dynamic value) {
_sessionValues[param] = value.toString();
}
}
/// Create a testing Usage instance.
///
/// All sent events, exceptions, timings, and pages are
/// buffered on the object and can be inspected later.
@visibleForTesting
class TestUsage implements Usage {
final commands = <TestUsageCommand>[];
final events = <TestUsageEvent>[];
final exceptions = <dynamic>[];
final timings = <TestTimingEvent>[];
var ensureAnalyticsSentCalls = 0;
var _printedWelcome = false;
@override
var enabled = true;
@override
var suppressAnalytics = false;
@override
String get clientId => 'test-client';
/// Confirms if the [printWelcome] method was invoked.
bool get printedWelcome => _printedWelcome;
@override
Future<void> ensureAnalyticsSent() async {
ensureAnalyticsSentCalls++;
}
@override
Stream<Map<String, dynamic>> get onSend => throw UnimplementedError();
@override
void printWelcome() {
_printedWelcome = true;
}
@override
void sendCommand(String command, {CustomDimensions? parameters}) {
commands.add(TestUsageCommand(command, parameters: parameters));
}
@override
void sendEvent(
String category,
String parameter, {
String? label,
int? value,
CustomDimensions? parameters,
}) {
events.add(
TestUsageEvent(category, parameter, label: label, value: value, parameters: parameters),
);
}
@override
void sendException(dynamic exception) {
exceptions.add(exception);
}
@override
void sendTiming(String category, String variableName, Duration duration, {String? label}) {
timings.add(TestTimingEvent(category, variableName, duration, label: label));
}
}
@visibleForTesting
@immutable
class TestUsageCommand {
const TestUsageCommand(this.command, {this.parameters});
final String command;
final CustomDimensions? parameters;
@override
bool operator ==(Object other) {
return other is TestUsageCommand && other.command == command && other.parameters == parameters;
}
@override
int get hashCode => Object.hash(command, parameters);
@override
String toString() => 'TestUsageCommand($command, parameters:$parameters)';
}
@visibleForTesting
@immutable
class TestUsageEvent {
const TestUsageEvent(this.category, this.parameter, {this.label, this.value, this.parameters});
final String category;
final String parameter;
final String? label;
final int? value;
final CustomDimensions? parameters;
@override
bool operator ==(Object other) {
return other is TestUsageEvent &&
other.category == category &&
other.parameter == parameter &&
other.label == label &&
other.value == value &&
other.parameters == parameters;
}
@override
int get hashCode => Object.hash(category, parameter, label, value, parameters);
@override
String toString() =>
'TestUsageEvent($category, $parameter, label:$label, value:$value, parameters:$parameters)';
}
@visibleForTesting
@immutable
class TestTimingEvent {
const TestTimingEvent(this.category, this.variableName, this.duration, {this.label});
final String category;
final String variableName;
final Duration duration;
final String? label;
@override
bool operator ==(Object other) {
return other is TestTimingEvent &&
other.category == category &&
other.variableName == variableName &&
other.duration == duration &&
other.label == label;
}
@override
int get hashCode => Object.hash(category, variableName, duration, label);
@override
String toString() => 'TestTimingEvent($category, $variableName, $duration, label:$label)';
}
bool _mapsEqual(Map<String, String>? a, Map<String, String>? b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
if (a.length != b.length) {
return false;
}
for (final String k in a.keys) {
final String? bValue = b[k];
if (bValue == null && !b.containsKey(k)) {
return false;
}
if (bValue != a[k]) {
return false;
}
}
return true;
}