analyze-sample-code.dart 13.1 KB
Newer Older
1 2 3 4
// 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.

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
// 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.
//
// 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.

40 41 42 43 44 45
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart' as path;

46 47
// To run this: bin/cache/dart-sdk/bin/dart dev/bots/analyze-sample-code.dart

48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
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* {
77 78
    if (preamble != null) {
      assert(!preamble.contains('\n'));
79
      yield preamble;
80 81
    }
    assert(!code.any((String line) => line.contains('\n')));
82
    yield* code;
83 84
    if (postamble != null) {
      assert(!postamble.contains('\n'));
85
      yield postamble;
86
    }
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
  }
  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;
105
  final List<String> buffer = <String>[];
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
  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) {
135
            if (!trimmedLine.startsWith(kDartDocPrefix) || trimmedLine.startsWith('/// ## ')) {
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
              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;
165
          } else if (trimmedLine == '/// ## Sample code' || trimmedLine == '/// ### Sample code') {
166 167 168 169 170 171 172 173
            inSampleSection = true;
            foundDart = false;
            sampleCodeSections += 1;
          }
        }
      }
    }
    buffer.add('// generated code');
174
    buffer.add('import \'dart:async\';');
175
    buffer.add('import \'dart:convert\';');
176
    buffer.add('import \'dart:math\' as math;');
177
    buffer.add('import \'dart:typed_data\';');
178
    buffer.add('import \'dart:ui\' as ui;');
179
    buffer.add('import \'package:flutter_test/flutter_test.dart\' hide TypeMatcher;');
180 181 182 183 184 185 186 187 188 189 190 191 192
    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('');
    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);
    }
193
    assert(buffer.length == lines.length);
194 195 196 197 198 199
    mainDart.writeAsStringSync(buffer.join('\n'));
    pubSpec.writeAsStringSync('''
name: analyze_sample_code
dependencies:
  flutter:
    sdk: flutter
200 201
  flutter_test:
    sdk: flutter
202 203 204 205 206 207 208 209
''');
    print('Found $sampleCodeSections sample code sections.');
    final Process process = await Process.start(
      _flutter,
      <String>['analyze', '--no-preamble', mainDart.path],
      workingDirectory: temp.path,
    );
    stderr.addStream(process.stderr);
210
    final List<String> errors = await process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList();
211 212
    if (errors.first == 'Building flutter tool...')
      errors.removeAt(0);
213 214 215 216 217 218 219 220
    if (errors.first.startsWith('Running "flutter packages get" in '))
      errors.removeAt(0);
    if (errors.first.startsWith('Analyzing '))
      errors.removeAt(0);
    if (errors.last.endsWith(' issues found.') || errors.last.endsWith(' issue found.'))
      errors.removeLast();
    int errorCount = 0;
    for (String error in errors) {
221
      final String kBullet = Platform.isWindows ? ' - ' : ' • ';
222
      const String kColon = ':';
223
      final RegExp atRegExp = new RegExp(r' at .*main.dart:');
224
      final int start = error.indexOf(kBullet);
225
      final int end = error.indexOf(atRegExp);
226 227
      if (start >= 0 && end >= 0) {
        final String message = error.substring(start + kBullet.length, end);
228 229
        final String atMatch = atRegExp.firstMatch(error)[0];
        final int colon2 = error.indexOf(kColon, end + atMatch.length);
230 231
        if (colon2 < 0) {
          keepMain = true;
232
          throw 'failed to parse error message: $error';
233
        }
234
        final String line = error.substring(end + atMatch.length, colon2);
235
        final int bullet2 = error.indexOf(kBullet, colon2);
236 237
        if (bullet2 < 0) {
          keepMain = true;
238
          throw 'failed to parse error message: $error';
239
        }
240 241 242
        final String column = error.substring(colon2 + kColon.length, bullet2);
        final int lineNumber = int.parse(line, radix: 10, onError: (String source) => throw 'failed to parse error message: $error');
        final int columnNumber = int.parse(column, radix: 10, onError: (String source) => throw 'failed to parse error message: $error');
243 244 245 246
        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';
        }
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
        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');
267
        keepMain = true;
268 269 270 271 272 273 274 275 276 277 278
        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).');
279 280 281 282 283 284 285
      print('-------8<-------');
      int number = 1;
      for (String line in buffer) {
        print('${number.toString().padLeft(6, " ")}: $line');
        number += 1;
      }
      print('-------8<-------');
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
    } else {
      temp.deleteSync(recursive: true);
    }
  }
  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(), ';'));
301 302 303 304
  } 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 ')) {
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
    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();
}