var Q = require('q'); var readline = require('readline'); var spawn = require('child_process').spawn; var path = require('path'); var glob = require('glob'); var fs = require('fs'); var util = require('./util'); var yaml = require('js-yaml'); module.exports = function(gulp, plugins, config) { return function() { var tempFile = '_analyzer.dart'; return util.forEachSubDirSequential(config.dest, function(dir) { var pubspecContents = fs.readFileSync(path.join(dir, 'pubspec.yaml')); var pubspec = yaml.safeLoad(pubspecContents); var packageName = pubspec.name; var libFiles = [].slice.call(glob.sync('lib/**/*.dart', {cwd: dir})); var webFiles = [].slice.call(glob.sync('web/**/*.dart', {cwd: dir})); var testFiles = [].slice.call(glob.sync('test/**/*_spec.dart', {cwd: dir})); var analyzeFile = ['library _analyzer;']; libFiles.concat(testFiles).concat(webFiles).forEach(function(fileName, index) { if (fileName !== tempFile && fileName.indexOf("/packages/") === -1) { if (fileName.indexOf('lib') == 0) { fileName = 'package:' + packageName + '/' + path.relative('lib', fileName).replace(/\\/g, '/'); } analyzeFile.push('import "' + fileName + '" as mod' + index + ';'); } }); fs.writeFileSync(path.join(dir, tempFile), analyzeFile.join('\n')); var defer = Q.defer(); analyze(dir, defer.makeNodeResolver()); return defer.promise; }); function analyze(dirName, done) { // TODO remove --package-warnings once dartanalyzer handles transitive libraries var args = ['--fatal-warnings', '--package-warnings', '--format=machine'].concat(tempFile); var stream = spawn(config.command, args, { // inherit stdin and stderr, but filter stdout stdio: [process.stdin, process.stdout, 'pipe'], cwd: dirName }); // Filter out unused imports from our generated file. // We don't reexports from the generated file // as this could lead to name clashes when two files // export the same thing. var rl = readline.createInterface( {input: stream.stderr, output: process.stdout, terminal: false}); var hintCount = 0; var errorCount = 0; var warningCount = 0; rl.on('line', function(line) { if (line == "find: > bin [: No such file or directory") { //Skip bad output from Dart SDK .bat files on Windows return; } var parsedLine = _AnalyzerOutputLine.parse(line); if (!parsedLine) { errorCount++; console.log('Unexpected output: ' + line); return; } // TODO remove once dartanalyzer handles transitive libraries // skip errors in third-party packages if (parsedLine.sourcePath.indexOf(dirName) == -1) { return; } if (parsedLine.shouldIgnore()) { return; } if (parsedLine.isHint) { hintCount++; } else if (parsedLine.isWarning) { warningCount++; } else { errorCount++; } console.log(dirName + ':' + parsedLine); }); stream.on('close', function() { var error; var report = []; if (errorCount > 0) { report.push(errorCount + ' error(s)'); } if (warningCount > 0) { report.push(warningCount + ' warning(s)'); } if (hintCount > 0) { report.push(hintCount + ' hint(s)'); } if (report.length > 0) { error = 'Dartanalyzer showed ' + report.join(', '); } done(error); }); } }; }; // See https://github.com/dart-lang/analyzer_cli/blob/master/lib/src/error_formatter.dart function _AnalyzerOutputLine(result) { this.severity = result[1]; this.errorType = result[2]; this.errorCode = result[3]; this.sourcePath = result[4]; this.lineNum = result[5]; this.colNum = result[6]; this.asciiLineLength = parseInt(result[7], 10); this.errorMsg = result[8]; this.isError = Boolean(this.severity.match(/ERROR/i)); this.isHint = Boolean(this.severity.match(/INFO/i)); this.isWarning = Boolean(this.severity.match(/WARNING/i)); } _AnalyzerOutputLine.parse = function(line) { var result = _AnalyzerOutputLine._analyzerParseRegExp.exec(line); return result ? new _AnalyzerOutputLine(result) : null; }; _AnalyzerOutputLine._analyzerParseRegExp = new RegExp('([^\|]+)\\|' + // #1, severity (NONE, INFO, WARNING, ERROR) '([^\|]+)\\|' + // #2, errorCode.type (HINT, *_WARNING, *_ERROR, etc) '([^\|]+)\\|' + // #3, errorCode (UNUSED_IMPORT, UNUSED_CATCH_STACK, etc) '([^\|]+[^\|\\\\])\\|' + // #4, sourcePath with '|' chars backslash-escaped. '([^\|]+)\\|' + // #5, line number '([^\|]+)\\|' + // #6, column number '([^\|]+)\\|' + // #7, length of the ASCII line to draw '(.*)$'); // #8, error message /* Maps file path (as string) to file source (an array of strings, one per line). */ _AnalyzerOutputLine.cache = {}; _AnalyzerOutputLine.ERR_NO_SOURCE = '(Could not find source line).'; _AnalyzerOutputLine.prototype = { toString: function() { var sourceLine = this._getSourceLine(); var lineText = _AnalyzerOutputLine.ERR_NO_SOURCE; if (sourceLine) { var repeat = function(str, num) { if (str.repeat) return str.repeat(num); return Array.prototype.join.call({length: num}, str); }; lineText = '\n' + sourceLine + '\n' + repeat(' ', this.colNum) + repeat('^', this.asciiLineLength); } return '[' + this.severity + '] ' + this.errorMsg + ' (' + this.sourcePath + ', line ' + this.lineNum + ', col ' + this.colNum + ')' + lineText; }, shouldIgnore: function() { if (this.errorCode.match(/UNUSED_IMPORT/i)) { if (this.sourcePath.match(/_analyzer\.dart/)) { return true; } } // TODO: https://github.com/angular/ts2dart/issues/168 if (this.errorCode.match(/UNUSED_CATCH_STACK/i)) { return true; } // Don't worry about hints in generated files. if (this.isHint && this.sourcePath.match(/generated/i)) { return true; } return false; }, // Reads the source file for the Analyzer output, caching it for future use. _getSourceLine: function() { var cache = _AnalyzerOutputLine.cache; var sourceLines = null; if (cache.hasOwnProperty(this.sourcePath)) { sourceLines = cache[this.sourcePath]; } else { try { sourceLines = String(fs.readFileSync(this.sourcePath)); sourceLines = sourceLines.split('\n'); } catch (e) { sourceLines = null; } finally { // Even if this fails, cache `null` so we don't try again. cache[this.sourcePath] = sourceLines; } } return sourceLines && this.lineNum <= sourceLines.length ? sourceLines[this.lineNum - 1] : null; } };