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 getFixes() => [_AddIsLocalFix()]; } class _AddIsLocalFix extends DartFix { @override void run( CustomLintResolver resolver, ChangeReporter reporter, CustomLintContext context, AnalysisError analysisError, List 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; }