blob: 59797acb5a7e23e3a0e729ce6f1e4ed1a5b1eace [file] [log] [blame]
// Copyright (c) 2018, 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 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import '../analyzer.dart';
const _cr = '\r';
const _desc = r'AVOID lines longer than 80 characters.';
const _details = r'''
**AVOID** lines longer than 80 characters
Readability studies show that long lines of text are harder to read because your
eye has to travel farther when moving to the beginning of the next line. This is
why newspapers and magazines use multiple columns of text.
If you really find yourself wanting lines longer than 80 characters, our
experience is that your code is likely too verbose and could be a little more
compact. The main offender is usually `VeryLongCamelCaseClassNames`. Ask
yourself, “Does each word in that type name tell me something critical or
prevent a name collision?” If not, consider omitting it.
Note that dartfmt does 99% of this for you, but the last 1% is you. It does not
split long string literals to fit in 80 columns, so you have to do that
manually.
We make an exception for URIs and file paths. When those occur in comments or
strings (usually in imports and exports), they may remain on a single line even
if they go over the line limit. This makes it easier to search source files for
a given path.
''';
const _lf = '\n';
/// String looks like URI if it contains a slash or backslash.
final _uriRegExp = RegExp(r'[/\\]');
bool _looksLikeUriOrPath(String value) => _uriRegExp.hasMatch(value);
class LinesLongerThan80Chars extends LintRule implements NodeLintRule {
LinesLongerThan80Chars()
: super(
name: 'lines_longer_than_80_chars',
description: _desc,
details: _details,
group: Group.style);
@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
final visitor = _Visitor(this, context);
registry.addCompilationUnit(this, visitor);
}
}
class _AllowedCommentVisitor extends SimpleAstVisitor {
final LineInfo lineInfo;
final allowedLines = <int>[];
_AllowedCommentVisitor(this.lineInfo);
@override
void visitCompilationUnit(CompilationUnit node) {
var token = node.beginToken;
while (token != null) {
_getPrecedingComments(token).forEach(_visitComment);
if (token == token.next) break;
token = token.next;
}
}
Iterable<Token> _getPrecedingComments(Token token) sync* {
var comment = token.precedingComments;
while (comment != null) {
yield comment;
comment = comment.next;
}
}
void _visitComment(Token comment) {
final content = comment.lexeme;
final lines = <String>[];
if (content.startsWith('///')) {
lines.add(content.substring(3));
} else if (content.startsWith('//')) {
final commentContent = content.substring(2);
if (commentContent.trimLeft().startsWith('ignore:')) {
allowedLines.add(lineInfo.getLocation(comment.offset).lineNumber);
} else {
lines.add(commentContent);
}
} else if (content.startsWith('/*')) {
// remove last slash before finding slash
lines.addAll(content
.substring(2, content.length - 2)
.split('$_cr$_lf')
.expand((e) => e.split(_cr))
.expand((e) => e.split(_lf)));
}
for (var i = 0; i < lines.length; i++) {
final value = lines[i];
if (_looksLikeUriOrPath(value)) {
final line = lineInfo.getLocation(comment.offset).lineNumber + i;
allowedLines.add(line);
}
}
}
}
class _AllowedLongLineVisitor extends RecursiveAstVisitor {
final LineInfo lineInfo;
final allowedLines = <int>[];
_AllowedLongLineVisitor(this.lineInfo);
@override
void visitSimpleStringLiteral(SimpleStringLiteral node) {
if (node.isMultiline) {
_handleMultilines(node);
} else {
_handleSingleLine(node, node.value);
}
}
@override
void visitStringInterpolation(StringInterpolation node) {
if (node.isMultiline) {
_handleMultilines(node);
} else {
final value = node.elements.map((e) {
if (e is InterpolationString) return e.value;
if (e is InterpolationExpression) return ' ' * e.length;
throw ArgumentError(
'Unhandled string interpolation element: ${node.runtimeType}');
}).join();
_handleSingleLine(node, value);
}
}
void _handleMultilines(SingleStringLiteral node) {
final startLine = lineInfo.getLocation(node.offset).lineNumber;
final endLine = lineInfo.getLocation(node.end).lineNumber;
for (var i = startLine; i <= endLine; i++) {
allowedLines.add(i);
}
}
void _handleSingleLine(AstNode node, String value) {
if (_looksLikeUriOrPath(value)) {
final line = lineInfo.getLocation(node.offset).lineNumber;
allowedLines.add(line);
}
}
}
class _LineInfo {
final int index;
final int offset;
final int end;
_LineInfo({this.index, this.offset, this.end});
int get length => end - offset;
}
class _Visitor extends SimpleAstVisitor {
final LintRule rule;
final LinterContext context;
_Visitor(this.rule, this.context);
@override
void visitCompilationUnit(CompilationUnit node) {
final lineInfo = node.lineInfo;
final lineCount = lineInfo.lineCount;
final longLines = <_LineInfo>[];
for (var i = 0; i < lineCount; i++) {
final start = lineInfo.getOffsetOfLine(i);
int end;
if (i == lineCount - 1) {
end = node.end;
} else {
end = lineInfo.getOffsetOfLine(i + 1) - 1;
final length = end - start;
if (length > 80) {
if (context.currentUnit.content[end] == _lf &&
context.currentUnit.content[end - 1] == _cr) {
end--;
}
}
}
final length = end - start;
if (length > 80) {
final line = _LineInfo(index: i, offset: start, end: end);
longLines.add(line);
}
}
if (longLines.isEmpty) return;
final allowedLineVisitor = _AllowedLongLineVisitor(lineInfo);
node.accept(allowedLineVisitor);
final allowedCommentVisitor = _AllowedCommentVisitor(lineInfo);
node.accept(allowedCommentVisitor);
final allowedLines = [
...allowedLineVisitor.allowedLines,
...allowedCommentVisitor.allowedLines
];
for (final line in longLines) {
if (allowedLines.contains(line.index + 1)) continue;
rule.reporter
.reportErrorForOffset(rule.lintCode, line.offset, line.length);
}
}
}