blob: e0c6f5cbc1269dd191a0a8640ab9bde2ad5fa58e [file] [log] [blame]
// 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.
// ignore: prefer_function_declarations_over_variables
void Function(String message) errorExit = (String message) {
io.stderr.writeln(message);
io.exit(1);
};