blob: 79d4eddfd63d0ff114e5cfa6270e50e92dfc41df [file] [log] [blame]
// Copyright (c) 2020, 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:_fe_analyzer_shared/src/scanner/token.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:nnbd_migration/src/edit_plan.dart';
/// Determines if the given [token] is followed by a nullability hint, and if
/// so, returns information about it. Otherwise returns `null`.
HintComment? getPostfixHint(Token token) {
var commentToken = token.next!.precedingComments;
if (commentToken != null) {
HintCommentKind kind;
if (commentToken.lexeme == '/*!*/') {
kind = HintCommentKind.bang;
} else if (commentToken.lexeme == '/*?*/') {
kind = HintCommentKind.question;
} else {
return null;
}
return HintComment(
kind,
token.end,
commentToken.offset,
commentToken.offset + '/*'.length,
commentToken.end - '*/'.length,
commentToken.end,
commentToken.end);
}
return null;
}
/// Determines if the given [token] is preceded by a hint, and if so, returns
/// information about it. Otherwise returns `null`.
HintComment? getPrefixHint(Token token) {
Token? commentToken = token.precedingComments;
if (commentToken != null) {
while (true) {
var nextComment = commentToken!.next;
if (nextComment == null) break;
commentToken = nextComment;
}
var lexeme = commentToken.lexeme;
if (lexeme.startsWith('/*') &&
lexeme.endsWith('*/') &&
lexeme.length >= '/*late*/'.length) {
var commentText =
lexeme.substring('/*'.length, lexeme.length - '*/'.length).trim();
var commentOffset = commentToken.offset;
if (commentText == 'late') {
var lateOffset = commentOffset + commentToken.lexeme.indexOf('late');
return HintComment(
HintCommentKind.late_,
commentOffset,
commentOffset,
lateOffset,
lateOffset + 'late'.length,
commentToken.end,
token.offset);
} else if (commentText == 'late final') {
var lateOffset = commentOffset + commentToken.lexeme.indexOf('late');
return HintComment(
HintCommentKind.lateFinal,
commentOffset,
commentOffset,
lateOffset,
lateOffset + 'late final'.length,
commentToken.end,
token.offset);
} else if (commentText == 'required') {
var requiredOffset =
commentOffset + commentToken.lexeme.indexOf('required');
return HintComment(
HintCommentKind.required,
commentOffset,
commentOffset,
requiredOffset,
requiredOffset + 'required'.length,
commentToken.end,
token.offset);
}
}
}
return null;
}
/// Information about a hint found in a source file.
class HintComment {
static final _identifierCharRegexp = RegExp('[a-zA-Z0-9_]');
/// What kind of hint this is.
final HintCommentKind kind;
/// The file offset of the first character that should be removed if the hint
/// is to be removed.
final int _removeOffset;
/// The file offset of the first character of the hint comment itself.
final int _commentOffset;
/// The file offset of the first character that should be kept if the hint is
/// to be replaced with the hinted text.
final int _keepOffset;
/// The file offset just beyond the last character that should be kept if the
/// hint is to be replaced with the hinted text.
final int _keepEnd;
/// The file offset just beyond the last character of the hint comment itself.
final int _commentEnd;
/// The file offset just beyond the last character that should be removed if
/// the hint is to be removed.
final int _removeEnd;
HintComment(this.kind, this._removeOffset, this._commentOffset,
this._keepOffset, this._keepEnd, this._commentEnd, this._removeEnd)
: assert(_removeOffset <= _commentOffset),
assert(_commentOffset < _keepOffset),
assert(_keepOffset < _keepEnd),
assert(_keepEnd < _commentEnd),
assert(_commentEnd <= _removeEnd);
/// Creates the changes necessary to accept the given hint (replace it with
/// its contents and fix up whitespace).
Map<int?, List<AtomicEdit>> changesToAccept(String? sourceText,
{AtomicEditInfo? info}) {
bool prependSpace = false;
bool appendSpace = false;
var removeOffset = _removeOffset;
var removeEnd = _removeEnd;
if (_isIdentifierCharBeforeOffset(sourceText, removeOffset) &&
_isIdentifierCharAtOffset(sourceText!, _keepOffset)) {
if (sourceText[removeOffset] == ' ') {
// We can just keep this space.
removeOffset++;
} else {
prependSpace = true;
}
}
if (_isIdentifierCharBeforeOffset(sourceText, _keepEnd) &&
_isIdentifierCharAtOffset(sourceText!, removeEnd)) {
if (sourceText[removeEnd - 1] == ' ') {
// We can just keep this space.
removeEnd--;
} else {
appendSpace = true;
}
}
return {
removeOffset: [
if (prependSpace) AtomicEdit.insert(' '),
AtomicEdit.delete(_keepOffset - removeOffset, info: info)
],
_keepEnd: [AtomicEdit.delete(removeEnd - _keepEnd, info: info)],
if (appendSpace) removeEnd: [AtomicEdit.insert(' ')]
};
}
/// Creates the changes necessary to remove the given hint (and fix up
/// whitespace).
Map<int?, List<AtomicEdit>> changesToRemove(String? sourceText,
{AtomicEditInfo? info}) {
bool appendSpace = false;
var removeOffset = _removeOffset;
if (_isIdentifierCharBeforeOffset(sourceText, removeOffset) &&
_isIdentifierCharAtOffset(sourceText!, _removeEnd)) {
if (sourceText[removeOffset] == ' ') {
// We can just keep this space.
removeOffset++;
} else {
appendSpace = true;
}
}
return {
removeOffset: [
AtomicEdit.delete(_removeEnd - removeOffset, info: info),
if (appendSpace) AtomicEdit.insert(' ')
]
};
}
/// Creates the changes necessary to replace the given hint with a different
/// hint.
Map<int?, List<AtomicEdit>> changesToReplace(
String? sourceText, String replacement,
{AtomicEditInfo? info}) {
return {
_commentOffset: [
AtomicEdit.replace(_commentEnd - _commentOffset, replacement,
info: info)
]
};
}
static bool _isIdentifierCharAtOffset(String sourceText, int offset) {
return offset < sourceText.length &&
_identifierCharRegexp.hasMatch(sourceText[offset]);
}
static bool _isIdentifierCharBeforeOffset(String? sourceText, int offset) {
return offset > 0 &&
_identifierCharRegexp.hasMatch(sourceText![offset - 1]);
}
}
/// Types of hint comments
enum HintCommentKind {
/// The comment `/*!*/`, which indicates that the type should not have a `?`
/// appended.
bang,
/// The comment `/*?*/`, which indicates that the type should have a `?`
/// appended.
question,
/// The comment `/*late*/`, which indicates that the variable declaration
/// should be late.
late_,
/// The comment `/*late final*/`, which indicates that the variable
/// declaration should be late and final.
lateFinal,
/// The comment `/*required*/`, which indicates that the parameter should be
/// required.
required,
}
extension FormalParameterExtensions on FormalParameter {
// TODO(srawlins): Add this to FormalParameter interface.
Token? get firstTokenAfterCommentAndMetadata {
var parameter = this is DefaultFormalParameter
? (this as DefaultFormalParameter).parameter
: this as NormalFormalParameter;
if (parameter is FieldFormalParameter) {
if (parameter.keyword != null) {
return parameter.keyword;
} else if (parameter.type != null) {
return parameter.type!.beginToken;
} else {
return parameter.thisKeyword;
}
} else if (parameter is FunctionTypedFormalParameter) {
if (parameter.covariantKeyword != null) {
return parameter.covariantKeyword;
} else if (parameter.returnType != null) {
return parameter.returnType!.beginToken;
} else {
return parameter.identifier.token;
}
} else if (parameter is SimpleFormalParameter) {
if (parameter.covariantKeyword != null) {
return parameter.covariantKeyword;
} else if (parameter.keyword != null) {
return parameter.keyword;
} else if (parameter.type != null) {
return parameter.type!.beginToken;
} else {
return parameter.identifier!.token;
}
}
return null;
}
}