diff --git a/lib/src/report/static_analysis.dart b/lib/src/report/static_analysis.dart index 7293cf223..f7296cfe0 100644 --- a/lib/src/report/static_analysis.dart +++ b/lib/src/report/static_analysis.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'dart:math' as math; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; @@ -77,24 +78,12 @@ Future<_AnalysisResult> _analyzePackage(PackageContext context) async { suggestion: 'To reproduce make sure you are using the [lints_core](https://pub.dev/packages/lints) and ' 'run `${context.usesFlutter ? 'flutter analyze' : 'dart analyze'} ${codeProblem.file}`', - spanFn: () { - final sourceFile = SourceFile.fromString( - File(p.join(context.packageDir, codeProblem.file)) - .readAsStringSync(), - url: p.join(context.packageDir, codeProblem.file)); - try { - // SourceSpans are 0-based, so we subtract 1 from line and column. - final startOffset = - sourceFile.getOffset(codeProblem.line - 1, codeProblem.col - 1); - // Limit the maximum length of the source span. - final length = math.min(codeProblem.length, maxSourceSpanLength); - return sourceFile.span(startOffset, startOffset + length); - } on RangeError { - // Note: This happens if the file contains CR as line terminators. - // If the range is invalid, then we just return null. - return null; - } - }, + spanFn: () => sourceSpanFromFile( + path: p.join(context.packageDir, codeProblem.file), + line: codeProblem.line, + col: codeProblem.col, + length: codeProblem.length, + ), ); } @@ -129,6 +118,46 @@ Future<_AnalysisResult> _analyzePackage(PackageContext context) async { } } +@visibleForTesting +FileSpan? sourceSpanFromFile({ + required String path, + required int line, + required int col, + required int length, +}) { + final sourceText = File(path).readAsStringSync(); + final sourceFile = SourceFile.fromString(sourceText, url: path); + try { + // SourceSpans are 0-based, so we subtract 1 from line and column. + var startOffset = sourceFile.getOffset(line - 1, col - 1); + + // making sure that we don't start on CR line terminator + // TODO: Remove this after https://github.com/dart-lang/source_span/issues/79 gets fixed. + while (startOffset < sourceText.length && + sourceText.codeUnitAt(startOffset) == 13) { + startOffset++; + } + // Limit the maximum length of the source span. + var newLength = math.min(length, maxSourceSpanLength); + newLength = math.min(newLength, sourceText.length - startOffset); + // making sure that we don't end on CR line terminator + // TODO: Remove this after https://github.com/dart-lang/source_span/issues/79 gets fixed. + while (newLength > 0 && + sourceText.codeUnitAt(startOffset + newLength - 1) == 13) { + newLength--; + } + if (newLength <= 0) { + // Note: this may happen if the span is entirely CR line terminators. + return null; + } + return sourceFile.span(startOffset, startOffset + newLength); + } on RangeError { + // Note: This happens if the file contains CR as line terminators. + // If the range is invalid, then we just return null. + return null; + } +} + class _AnalysisResult { final List errors; final List warnings; diff --git a/test/report/static_analysis_test.dart b/test/report/static_analysis_test.dart new file mode 100644 index 000000000..4f0548f33 --- /dev/null +++ b/test/report/static_analysis_test.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. 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:io'; + +import 'package:pana/src/report/static_analysis.dart'; +import 'package:pana/src/utils.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + test('bad cr position', () async { + await withTempDir((dir) async { + final file = File(p.join(dir, 'cr.txt')); + await file.writeAsString('abcd efgh\r\n\r\n1234\r\n\r\nxyz'); + final s1 = + sourceSpanFromFile(path: file.path, line: 1, col: 6, length: 7); + expect(s1!.text, 'efgh\r\n'); + final s2 = + sourceSpanFromFile(path: file.path, line: 1, col: 10, length: 8); + expect(s2!.text, '\n\r\n1234'); + }); + }); +}