// 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? _cachedFlutterInformation; Map 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( [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 flutterVersion = json.decode(flutterVersionJson) as Map; 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 info = {}; info['flutterRoot'] = filesystem.directory(flutterVersion['flutterRoot']! as String); info['frameworkVersion'] = Version.parse(flutterVersion['frameworkVersion'] as String); final RegExpMatch? dartVersionRegex = RegExp(r'(?[\d.]+)(?:\s+\(build (?[-.\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 injections, String template, Map metadata, { bool addCopyright = false, }) { String wrapSectionMarker(Iterable 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 = [ ...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 : [], 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 getSamplesInElements(Iterable? elements) { return elements ?.expand((SourceElement element) => element.samples) ?? const []; } 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 = [ 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 = [ '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); };