// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This script analyzes all the sample code in API docs in the Flutter source. // // It uses the following conventions: // // Code is denoted by markdown ```dart / ``` markers. // // Only code in "## Sample code" or "### Sample code" sections is examined. // Subheadings can also be specified, as in "## Sample code: foo". // // There are several kinds of sample code you can specify: // // * Constructor calls, typically showing what might exist in a build method. // These start with "new" or "const", and will be inserted into an assignment // expression assigning to a variable of type "dynamic" and followed by a // semicolon, for the purposes of analysis. // // * Class definitions. These start with "class", and are analyzed verbatim. // // * Other code. It gets included verbatim, though any line that says "// ..." // is considered to separate the block into multiple blocks to be processed // individually. // // In addition, you can declare code that should be included in the analysis but // not shown in the API docs by adding a comment "// Examples can assume:" to // the file (usually at the top of the file, after the imports), following by // one or more commented-out lines of code. That code is included verbatim in // the analysis. // // All the sample code of every file is analyzed together. This means you can't // have two pieces of sample code that define the same example class. // // Also, the above means that it's tricky to include verbatim imperative code // (e.g. a call to a method), since it won't be valid to have such code at the // top level. Instead, wrap it in a function or even a whole class, or make it a // valid variable declaration. import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; // To run this: bin/cache/dart-sdk/bin/dart dev/bots/analyze-sample-code.dart final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script)))); final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'); class Line { const Line(this.filename, this.line, this.indent); final String filename; final int line; final int indent; Line get next => this + 1; Line operator +(int count) { if (count == 0) return this; return new Line(filename, line + count, indent); } @override String toString([int column]) { if (column != null) return '$filename:$line:${column + indent}'; return '$filename:$line'; } } class Section { const Section(this.start, this.preamble, this.code, this.postamble); final Line start; final String preamble; final List<String> code; final String postamble; Iterable<String> get strings sync* { if (preamble != null) { assert(!preamble.contains('\n')); yield preamble; } assert(!code.any((String line) => line.contains('\n'))); yield* code; if (postamble != null) { assert(!postamble.contains('\n')); yield postamble; } } List<Line> get lines { final List<Line> result = new List<Line>.generate(code.length, (int index) => start + index); if (preamble != null) result.insert(0, null); if (postamble != null) result.add(null); return result; } } const String kDartDocPrefix = '///'; const String kDartDocPrefixWithSpace = '$kDartDocPrefix '; Future<Null> main() async { final Directory temp = Directory.systemTemp.createTempSync('analyze_sample_code_'); int exitCode = 1; bool keepMain = false; final List<String> buffer = <String>[]; try { final File mainDart = new File(path.join(temp.path, 'main.dart')); final File pubSpec = new File(path.join(temp.path, 'pubspec.yaml')); final Directory flutterPackage = new Directory(path.join(_flutterRoot, 'packages', 'flutter', 'lib')); final List<Section> sections = <Section>[]; int sampleCodeSections = 0; for (FileSystemEntity file in flutterPackage.listSync(recursive: true, followLinks: false)) { if (file is File && path.extension(file.path) == '.dart') { final List<String> lines = file.readAsLinesSync(); bool inPreamble = false; bool inSampleSection = false; bool inDart = false; bool foundDart = false; int lineNumber = 0; final List<String> block = <String>[]; Line startLine; for (String line in lines) { lineNumber += 1; final String trimmedLine = line.trim(); if (inPreamble) { if (line.isEmpty) { inPreamble = false; processBlock(startLine, block, sections); } else if (!line.startsWith('// ')) { throw '${file.path}:$lineNumber: Unexpected content in sample code preamble.'; } else { block.add(line.substring(3)); } } else if (inSampleSection) { if (!trimmedLine.startsWith(kDartDocPrefix) || trimmedLine.startsWith('/// ## ')) { if (inDart) throw '${file.path}:$lineNumber: Dart section inexplicably unterminated.'; if (!foundDart) throw '${file.path}:$lineNumber: No dart block found in sample code section'; inSampleSection = false; } else { if (inDart) { if (trimmedLine == '/// ```') { inDart = false; processBlock(startLine, block, sections); } else if (trimmedLine == kDartDocPrefix) { block.add(''); } else { final int index = line.indexOf(kDartDocPrefixWithSpace); if (index < 0) throw '${file.path}:$lineNumber: Dart section inexplicably did not contain "$kDartDocPrefixWithSpace" prefix.'; block.add(line.substring(index + 4)); } } else if (trimmedLine == '/// ```dart') { assert(block.isEmpty); startLine = new Line(file.path, lineNumber + 1, line.indexOf(kDartDocPrefixWithSpace) + kDartDocPrefixWithSpace.length); inDart = true; foundDart = true; } } } else if (line == '// Examples can assume:') { assert(block.isEmpty); startLine = new Line(file.path, lineNumber + 1, 3); inPreamble = true; } else if (trimmedLine == '/// ## Sample code' || trimmedLine.startsWith('/// ## Sample code:') || trimmedLine == '/// ### Sample code' || trimmedLine.startsWith('/// ### Sample code:')) { inSampleSection = true; foundDart = false; sampleCodeSections += 1; } } } } buffer.add('// generated code'); buffer.add('import \'dart:async\';'); buffer.add('import \'dart:convert\';'); buffer.add('import \'dart:math\' as math;'); buffer.add('import \'dart:typed_data\';'); buffer.add('import \'dart:ui\' as ui;'); buffer.add('import \'package:flutter_test/flutter_test.dart\';'); for (FileSystemEntity file in flutterPackage.listSync(recursive: false, followLinks: false)) { if (file is File && path.extension(file.path) == '.dart') { buffer.add(''); buffer.add('// ${file.path}'); buffer.add('import \'package:flutter/${path.basename(file.path)}\';'); } } buffer.add(''); buffer.add('// ignore_for_file: unused_element'); buffer.add(''); final List<Line> lines = new List<Line>.filled(buffer.length, null, growable: true); for (Section section in sections) { buffer.addAll(section.strings); lines.addAll(section.lines); } assert(buffer.length == lines.length); mainDart.writeAsStringSync(buffer.join('\n')); pubSpec.writeAsStringSync(''' name: analyze_sample_code dependencies: flutter: sdk: flutter flutter_test: sdk: flutter '''); print('Found $sampleCodeSections sample code sections.'); final Process process = await Process.start( _flutter, <String>['analyze', '--no-preamble', '--no-congratulate', mainDart.parent.path], workingDirectory: temp.path, ); stderr.addStream(process.stderr); final List<String> errors = await process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList(); if (errors.first == 'Building flutter tool...') errors.removeAt(0); if (errors.first.startsWith('Running "flutter packages get" in ')) errors.removeAt(0); int errorCount = 0; for (String error in errors) { final String kBullet = Platform.isWindows ? ' - ' : ' • '; const String kColon = ':'; final RegExp atRegExp = new RegExp(r' at .*main.dart:'); final int start = error.indexOf(kBullet); final int end = error.indexOf(atRegExp); if (start >= 0 && end >= 0) { final String message = error.substring(start + kBullet.length, end); final String atMatch = atRegExp.firstMatch(error)[0]; final int colon2 = error.indexOf(kColon, end + atMatch.length); if (colon2 < 0) { keepMain = true; throw 'failed to parse error message: $error'; } final String line = error.substring(end + atMatch.length, colon2); final int bullet2 = error.indexOf(kBullet, colon2); if (bullet2 < 0) { keepMain = true; throw 'failed to parse error message: $error'; } final String column = error.substring(colon2 + kColon.length, bullet2); final int lineNumber = int.tryParse(line, radix: 10); final int columnNumber = int.tryParse(column, radix: 10); if (lineNumber == null) { throw 'failed to parse error message: $error'; } if (columnNumber == null) { throw 'failed to parse error message: $error'; } if (lineNumber < 1 || lineNumber > lines.length) { keepMain = true; throw 'failed to parse error message (read line number as $lineNumber; total number of lines is ${lines.length}): $error'; } final Line actualLine = lines[lineNumber - 1]; final String errorCode = error.substring(bullet2 + kBullet.length); if (errorCode == 'unused_element') { // We don't really care if sample code isn't used! } else if (actualLine == null) { if (errorCode == 'missing_identifier' && lineNumber > 1 && buffer[lineNumber - 2].endsWith(',')) { final Line actualLine = lines[lineNumber - 2]; print('${actualLine.toString(buffer[lineNumber - 2].length - 1)}: unexpected comma at end of sample code'); errorCount += 1; } else { print('${mainDart.path}:${lineNumber - 1}:$columnNumber: $message'); keepMain = true; errorCount += 1; } } else { print('${actualLine.toString(columnNumber)}: $message ($errorCode)'); errorCount += 1; } } else { print('?? $error'); keepMain = true; errorCount += 1; } } exitCode = await process.exitCode; if (exitCode == 1 && errorCount == 0) exitCode = 0; if (exitCode == 0) print('No errors!'); } finally { if (keepMain) { print('Kept ${temp.path} because it had errors (see above).'); print('-------8<-------'); int number = 1; for (String line in buffer) { print('${number.toString().padLeft(6, " ")}: $line'); number += 1; } print('-------8<-------'); } else { try { temp.deleteSync(recursive: true); } on FileSystemException catch (e) { // ignore errors deleting the temporary directory print('Ignored exception during tearDown: $e'); } } } exit(exitCode); } int _expressionId = 0; void processBlock(Line line, List<String> block, List<Section> sections) { if (block.isEmpty) throw '$line: Empty ```dart block in sample code.'; if (block.first.startsWith('new ') || block.first.startsWith('const ')) { _expressionId += 1; sections.add(new Section(line, 'dynamic expression$_expressionId = ', block.toList(), ';')); } else if (block.first.startsWith('await ')) { _expressionId += 1; sections.add(new Section(line, 'Future<Null> expression$_expressionId() async { ', block.toList(), ' }')); } else if (block.first.startsWith('class ')) { sections.add(new Section(line, null, block.toList(), null)); } else { final List<String> buffer = <String>[]; int subblocks = 0; Line subline; for (int index = 0; index < block.length; index += 1) { if (block[index] == '' || block[index] == '// ...') { if (subline == null) throw '${line + index}: Unexpected blank line or "// ..." line near start of subblock in sample code.'; subblocks += 1; processBlock(subline, buffer, sections); assert(buffer.isEmpty); subline = null; } else if (block[index].startsWith('// ')) { if (buffer.length > 1) // don't include leading comments buffer.add('/${block[index]}'); // so that it doesn't start with "// " and get caught in this again } else { subline ??= line + index; buffer.add(block[index]); } } if (subblocks > 0) { if (subline != null) processBlock(subline, buffer, sections); } else { sections.add(new Section(line, null, block.toList(), null)); } } block.clear(); }