mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] 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
267 lines
8.9 KiB
Dart
267 lines
8.9 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.
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io' as io;
|
|
|
|
import 'package:file/file.dart';
|
|
import 'package:file/local.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:platform/platform.dart' show LocalPlatform, Platform;
|
|
import 'package:process/process.dart' show LocalProcessManager, ProcessManager;
|
|
import 'package:pub_semver/pub_semver.dart';
|
|
|
|
import 'data_types.dart';
|
|
|
|
/// An exception class to allow capture of exceptions generated by the Snippets
|
|
/// package.
|
|
class SnippetException implements Exception {
|
|
SnippetException(this.message, {this.file, this.line});
|
|
final String message;
|
|
final String? file;
|
|
final int? line;
|
|
|
|
@override
|
|
String toString() {
|
|
if (file != null || line != null) {
|
|
final fileStr = file == null ? '' : '$file:';
|
|
final lineStr = line == null ? '' : '$line:';
|
|
return '$runtimeType: $fileStr$lineStr: $message';
|
|
} else {
|
|
return '$runtimeType: $message';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Gets the number of whitespace characters at the beginning of a line.
|
|
int getIndent(String line) => line.length - line.trimLeft().length;
|
|
|
|
/// Contains information about the installed Flutter repo.
|
|
class FlutterInformation {
|
|
FlutterInformation({
|
|
this.platform = const LocalPlatform(),
|
|
this.processManager = const LocalProcessManager(),
|
|
this.filesystem = const LocalFileSystem(),
|
|
});
|
|
|
|
final Platform platform;
|
|
final ProcessManager processManager;
|
|
final FileSystem filesystem;
|
|
|
|
static FlutterInformation? _instance;
|
|
|
|
static FlutterInformation get instance => _instance ??= FlutterInformation();
|
|
|
|
@visibleForTesting
|
|
static set instance(FlutterInformation? value) => _instance = value;
|
|
|
|
Directory getFlutterRoot() {
|
|
if (platform.environment['FLUTTER_ROOT'] != null) {
|
|
return filesystem.directory(platform.environment['FLUTTER_ROOT']);
|
|
}
|
|
return getFlutterInformation()['flutterRoot'] as Directory;
|
|
}
|
|
|
|
Version getFlutterVersion() => getFlutterInformation()['frameworkVersion'] as Version;
|
|
|
|
Version getDartSdkVersion() => getFlutterInformation()['dartSdkVersion'] as Version;
|
|
|
|
Map<String, dynamic>? _cachedFlutterInformation;
|
|
|
|
Map<String, dynamic> getFlutterInformation() {
|
|
if (_cachedFlutterInformation != null) {
|
|
return _cachedFlutterInformation!;
|
|
}
|
|
|
|
String flutterVersionJson;
|
|
if (platform.environment['FLUTTER_VERSION'] != null) {
|
|
flutterVersionJson = platform.environment['FLUTTER_VERSION']!;
|
|
} else {
|
|
String flutterCommand;
|
|
if (platform.environment['FLUTTER_ROOT'] != null) {
|
|
flutterCommand = filesystem
|
|
.directory(platform.environment['FLUTTER_ROOT'])
|
|
.childDirectory('bin')
|
|
.childFile('flutter')
|
|
.absolute
|
|
.path;
|
|
} else {
|
|
flutterCommand = 'flutter';
|
|
}
|
|
io.ProcessResult result;
|
|
try {
|
|
result = processManager.runSync(<String>[
|
|
flutterCommand,
|
|
'--version',
|
|
'--machine',
|
|
], stdoutEncoding: utf8);
|
|
} on io.ProcessException catch (e) {
|
|
throw SnippetException(
|
|
'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place flutter command in your path.\n$e',
|
|
);
|
|
}
|
|
if (result.exitCode != 0) {
|
|
throw SnippetException(
|
|
'Unable to determine Flutter information, because of abnormal exit to flutter command.',
|
|
);
|
|
}
|
|
flutterVersionJson = (result.stdout as String).replaceAll(
|
|
'Waiting for another flutter command to release the startup lock...',
|
|
'',
|
|
);
|
|
}
|
|
|
|
final flutterVersion = json.decode(flutterVersionJson) as Map<String, dynamic>;
|
|
if (flutterVersion['flutterRoot'] == null ||
|
|
flutterVersion['frameworkVersion'] == null ||
|
|
flutterVersion['dartSdkVersion'] == null) {
|
|
throw SnippetException(
|
|
'Flutter command output has unexpected format, unable to determine flutter root location.',
|
|
);
|
|
}
|
|
|
|
final info = <String, dynamic>{};
|
|
info['flutterRoot'] = filesystem.directory(flutterVersion['flutterRoot']! as String);
|
|
info['frameworkVersion'] = Version.parse(flutterVersion['frameworkVersion'] as String);
|
|
|
|
final RegExpMatch? dartVersionRegex = RegExp(
|
|
r'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\w]+)\))?',
|
|
).firstMatch(flutterVersion['dartSdkVersion'] as String);
|
|
if (dartVersionRegex == null) {
|
|
throw SnippetException(
|
|
'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.',
|
|
);
|
|
}
|
|
info['dartSdkVersion'] = Version.parse(
|
|
dartVersionRegex.namedGroup('detail') ?? dartVersionRegex.namedGroup('base')!,
|
|
);
|
|
_cachedFlutterInformation = info;
|
|
|
|
return info;
|
|
}
|
|
}
|
|
|
|
/// Injects the [injections] into the [template], while turning the
|
|
/// "description" injection into a comment.
|
|
String interpolateTemplate(
|
|
List<SkeletonInjection> injections,
|
|
String template,
|
|
Map<String, Object?> metadata, {
|
|
bool addCopyright = false,
|
|
}) {
|
|
String wrapSectionMarker(Iterable<String> contents, {required String name}) {
|
|
if (contents.join().trim().isEmpty) {
|
|
// Skip empty sections.
|
|
return '';
|
|
}
|
|
// We don't wrap some sections, because otherwise they generate invalid files.
|
|
final String result = <String>[...contents].join('\n');
|
|
final wrappingNewlines = RegExp(r'^\n*(.*)\n*$', dotAll: true);
|
|
return result.replaceAllMapped(wrappingNewlines, (Match match) => match.group(1)!);
|
|
}
|
|
|
|
return '${addCopyright ? '{{copyright}}\n\n' : ''}$template'
|
|
.replaceAllMapped(RegExp(r'{{([^}]+)}}'), (Match match) {
|
|
final String name = match[1]!;
|
|
final int componentIndex = injections.indexWhere(
|
|
(SkeletonInjection injection) => injection.name == name,
|
|
);
|
|
if (metadata[name] != null && componentIndex == -1) {
|
|
// If the match isn't found in the injections, then just return the
|
|
// metadata entry.
|
|
return wrapSectionMarker((metadata[name]! as String).split('\n'), name: name);
|
|
}
|
|
return wrapSectionMarker(
|
|
componentIndex >= 0 ? injections[componentIndex].stringContents : <String>[],
|
|
name: name,
|
|
);
|
|
})
|
|
.replaceAll(RegExp(r'\n\n+'), '\n\n');
|
|
}
|
|
|
|
class SampleStats {
|
|
const SampleStats({
|
|
this.totalSamples = 0,
|
|
this.dartpadSamples = 0,
|
|
this.snippetSamples = 0,
|
|
this.applicationSamples = 0,
|
|
this.wordCount = 0,
|
|
this.lineCount = 0,
|
|
this.linkCount = 0,
|
|
this.description = '',
|
|
});
|
|
|
|
final int totalSamples;
|
|
final int dartpadSamples;
|
|
final int snippetSamples;
|
|
final int applicationSamples;
|
|
final int wordCount;
|
|
final int lineCount;
|
|
final int linkCount;
|
|
final String description;
|
|
bool get allOneKind =>
|
|
totalSamples == snippetSamples ||
|
|
totalSamples == applicationSamples ||
|
|
totalSamples == dartpadSamples;
|
|
|
|
@override
|
|
String toString() {
|
|
return description;
|
|
}
|
|
}
|
|
|
|
Iterable<CodeSample> getSamplesInElements(Iterable<SourceElement>? elements) {
|
|
return elements?.expand<CodeSample>((SourceElement element) => element.samples) ??
|
|
const <CodeSample>[];
|
|
}
|
|
|
|
SampleStats getSampleStats(SourceElement element) {
|
|
if (element.comment.isEmpty) {
|
|
return const SampleStats();
|
|
}
|
|
final int total = element.sampleCount;
|
|
if (total == 0) {
|
|
return const SampleStats();
|
|
}
|
|
final int dartpads = element.dartpadSampleCount;
|
|
final int snippets = element.snippetCount;
|
|
final int applications = element.applicationSampleCount;
|
|
final String sampleCount = <String>[
|
|
if (snippets > 0) '$snippets snippet${snippets != 1 ? 's' : ''}',
|
|
if (applications > 0) '$applications application sample${applications != 1 ? 's' : ''}',
|
|
if (dartpads > 0) '$dartpads dartpad sample${dartpads != 1 ? 's' : ''}',
|
|
].join(', ');
|
|
final int wordCount = element.wordCount;
|
|
final int lineCount = element.lineCount;
|
|
final int linkCount = element.referenceCount;
|
|
final String description = <String>[
|
|
'Documentation has $wordCount ${wordCount == 1 ? 'word' : 'words'} on ',
|
|
'$lineCount ${lineCount == 1 ? 'line' : 'lines'}',
|
|
if (linkCount > 0 && element.hasSeeAlso) ', ',
|
|
if (linkCount > 0 && !element.hasSeeAlso) ' and ',
|
|
if (linkCount > 0) 'refers to $linkCount other ${linkCount == 1 ? 'symbol' : 'symbols'}',
|
|
if (linkCount > 0 && element.hasSeeAlso) ', and ',
|
|
if (linkCount == 0 && element.hasSeeAlso) 'and ',
|
|
if (element.hasSeeAlso) 'has a "See also:" section',
|
|
'.',
|
|
].join();
|
|
return SampleStats(
|
|
totalSamples: total,
|
|
dartpadSamples: dartpads,
|
|
snippetSamples: snippets,
|
|
applicationSamples: applications,
|
|
wordCount: wordCount,
|
|
lineCount: lineCount,
|
|
linkCount: linkCount,
|
|
description: 'Has $sampleCount. $description',
|
|
);
|
|
}
|
|
|
|
/// Exit the app with a message to stderr.
|
|
/// Can be overridden by tests to avoid exits.
|
|
void Function(String message) errorExit = (String message) {
|
|
io.stderr.writeln(message);
|
|
io.exit(1);
|
|
};
|