mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Powered by the new and amazing `unnecessary_ignore` lint. We're not enabling this lint by default because being able to temporarily use ignores that don't ignore anything is a powerful tool to enable migrations. We should turn this lint on locally periodically, though, and clean up all outdated ignores.
270 lines
9.1 KiB
Dart
270 lines
9.1 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 String fileStr = file == null ? '' : '$file:';
|
|
final String 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 Map<String, dynamic> 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 Map<String, dynamic> 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 RegExp 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);
|
|
};
|