Skip to content

Instantly share code, notes, and snippets.

@MarcinusX
Last active March 6, 2023 09:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MarcinusX/9548f66251c4784aeb2de5eeaa7c9da5 to your computer and use it in GitHub Desktop.
Save MarcinusX/9548f66251c4784aeb2de5eeaa7c9da5 to your computer and use it in GitHub Desktop.
A linter for parsing protobuf's Timestamps to DateTime. The goal is to always include isLocal: true when doing it.
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
class PreferTimestampsToLocalDateTimes extends DartLintRule {
PreferTimestampsToLocalDateTimes() : super(code: _code);
static const _code = LintCode(
name: 'timestamp_to_date_time_is_local',
problemMessage:
'Parsing Timestamp to DateTime should always be to local time',
);
@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addMethodInvocation((node) {
if (node.isFromTimestamp &&
node.isToDateTime &&
!node.hasArgumentToLocalSetToTrue) {
reporter.reportErrorForNode(_code, node);
}
});
}
@override
List<Fix> getFixes() => [_AddIsLocalFix()];
}
class _AddIsLocalFix extends DartFix {
@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
AnalysisError analysisError,
List<AnalysisError> others,
) {
context.registry.addMethodInvocation((node) {
if (!analysisError.sourceRange.intersects(node.sourceRange) ||
!node.isFromTimestamp && !node.isToDateTime) {
return;
}
final leftParenthesisOffset = node.argumentList.leftParenthesis.offset;
final rightParenthesisOffset = node.argumentList.rightParenthesis.offset;
final fix = 'toLocal: true';
if (node.hasArgumentToLocalSetToFalse) {
final changeBuilder = reporter.createChangeBuilder(
message: 'Replace with "toLocal: true"',
priority: 1,
);
changeBuilder.addDartFileEdit((builder) {
builder.addSimpleReplacement(
SourceRange(
leftParenthesisOffset + 1,
rightParenthesisOffset - leftParenthesisOffset - 1,
),
fix,
);
});
} else {
final changeBuilder = reporter.createChangeBuilder(
message: 'Add "toLocal: true"',
priority: 1,
);
changeBuilder.addDartFileEdit((builder) {
builder.addSimpleInsertion(
leftParenthesisOffset + 1,
fix,
);
});
}
});
}
}
extension on MethodInvocation {
DartType? get targetType => target?.staticType;
bool get isFromTimestamp =>
targetType != null &&
TypeChecker.fromName(
'Timestamp',
packageName: 'cheddar_api',
).isAssignableFromType(targetType!);
bool get isToDateTime => methodName.name == 'toDateTime';
bool get hasArgumentToLocalSetToTrue => hasArgumentToLocalSetToValue(true);
bool get hasArgumentToLocalSetToFalse => hasArgumentToLocalSetToValue(false);
bool hasArgumentToLocalSetToValue(bool value) =>
argumentList.arguments.any((argument) =>
argument is NamedExpression &&
argument.name.label.name == 'toLocal' &&
argument.expression.isBooleanWithValue(value));
}
extension on Expression {
bool isBooleanWithValue(bool value) =>
this is BooleanLiteral && (this as BooleanLiteral).value == value;
}
@rrousselGit
Copy link

Make sure to check the package name too when using TypeChecker.fromName('Timestamp'). You wouldn't want a class with the Timestamp name from a different package to be impacted by your lint

@MarcinusX
Copy link
Author

Make sure to check the package name too when using TypeChecker.fromName('Timestamp'). You wouldn't want a class with the Timestamp name from a different package to be impacted by your lint

Right! Thanks! Updated :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment