diff --git a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart index 7d3ed4757e..0b6d28b5a8 100644 --- a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart +++ b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart @@ -1,5 +1,8 @@ -import 'package:analyzer/error/error.dart' show ErrorSeverity; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element2.dart'; +import 'package:analyzer/error/error.dart' show ErrorSeverity, AnalysisError; import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/source/source_range.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; // ignore: depend_on_referenced_packages import 'package:glob/glob.dart'; @@ -20,6 +23,10 @@ class ImmichLinter extends PluginBase { buildGlob(forbiddenPaths), restrict)); } } + + if (configs.rules[NoDebugPrintRule.name]?.enabled ?? true) { + rules.add(NoDebugPrintRule()); + } return rules; } @@ -87,3 +94,150 @@ class ImportRule extends DartLintRule { }); } } + +class NoDebugPrintRule extends DartLintRule { + static const name = 'no_debug_print'; + static const importPath = 'package:flutter/src/foundation/print.dart'; + static const _code = LintCode( + name: name, + problemMessage: + 'Use dPrint instead of debugPrint for proper tree-shaking in release builds.', + correctionMessage: 'Replace debugPrint with dPrint', + errorSeverity: ErrorSeverity.WARNING, + ); + + NoDebugPrintRule() : super(code: _code); + + @pragma('vm:prefer-inline') + static bool isDebugPrint(Element2? element) { + return element is PropertyAccessorElement2 && + element.variable3?.getter2?.name3 == 'debugPrint' && + element.library2.identifier == importPath; + } + + @pragma('vm:prefer-inline') + static bool isWrapped(AstNode node) { + AstNode? parent = node.parent; + while (parent != null) { + if (parent case IfStatement(:SimpleIdentifier expression) + when expression.name == 'kDebugMode') { + return true; + } + parent = parent.parent; + } + + return false; + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addFunctionExpressionInvocation((node) { + final function = node.function; + if (function case SimpleIdentifier(:final element) + when isDebugPrint(element) && !isWrapped(node)) { + reporter.atNode(function, code); + } + }); + + context.registry.addMethodInvocation((node) { + final methodName = node.methodName; + if (isDebugPrint(methodName.element) && !isWrapped(node)) { + reporter.atNode(methodName, code); + } + }); + } + + @override + List getFixes() => [ReplaceDebugPrintFix()]; +} + +class ReplaceDebugPrintFix extends DartFix { + static const dPrintImportPath = + "import 'package:immich_mobile/utils/debug_print.dart';"; + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError error, + List allErrors, + ) { + context.registry.addFunctionExpressionInvocation((node) { + final function = node.function; + if (error.sourceRange == function.sourceRange && + function is SimpleIdentifier && + NoDebugPrintRule.isDebugPrint(function.element)) { + _createFix(reporter, resolver, function, node); + } + }); + + context.registry.addMethodInvocation((node) { + final methodName = node.methodName; + if (error.sourceRange == methodName.sourceRange && + NoDebugPrintRule.isDebugPrint(methodName.element)) { + _createFix(reporter, resolver, methodName, node); + } + }); + } + + void _createFix( + ChangeReporter reporter, + CustomLintResolver resolver, + SimpleIdentifier identifier, + AstNode node, + ) { + final arguments = switch (node) { + MethodInvocation(:final argumentList) => argumentList, + FunctionExpressionInvocation(:final argumentList) => argumentList, + _ => null, + }; + if (arguments == null) { + return; + } + + final changeBuilder = reporter.createChangeBuilder( + message: 'Replace with dPrint', + priority: 1, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addSimpleReplacement(identifier.sourceRange, 'dPrint'); + final firstArg = arguments.arguments.firstOrNull; + if (firstArg != null) { + final argSource = resolver.source.contents.data.substring( + firstArg.offset, + firstArg.end, + ); + + builder.addSimpleReplacement( + SourceRange(firstArg.offset, firstArg.length), + '() => $argSource', + ); + } + + if (resolver.source.contents.data.contains(dPrintImportPath)) { + return; + } + + final unit = node.root; + if (unit is CompilationUnit) { + final lastImport = + unit.directives.whereType().lastOrNull; + + if (lastImport != null) { + builder.addSimpleInsertion(lastImport.end, '\n$dPrintImportPath'); + } else if (unit.directives.isNotEmpty) { + builder.addSimpleInsertion( + unit.directives.last.end, '\n\n$dPrintImportPath'); + } else { + builder.addSimpleInsertion(0, '$dPrintImportPath\n\n'); + } + } + }); + } +} diff --git a/mobile/lib/utils/debug_print.dart b/mobile/lib/utils/debug_print.dart new file mode 100644 index 0000000000..21f55fc6a5 --- /dev/null +++ b/mobile/lib/utils/debug_print.dart @@ -0,0 +1,8 @@ +import 'package:flutter/foundation.dart'; + +@pragma('vm:prefer-inline') +void dPrint(String Function() message) { + if (kDebugMode) { + debugPrint(message()); + } +}