如何使用source_gen和build_runner以字符串形式获取函数体?

问题描述 投票:0回答:2

我的目标是让我的单元测试易于理解。目前,它们很难理解,因为它们有太多嵌套函数。

我想使用 build_runner 生成单元的代码,并解开所有函数。

这是我当前测试的一个例子:

测试.dart

import 'package:example_usage/src/unwrap.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class Cat {
  String sound() => "Meow";
  int walk() => 4;
}

class Dog {
  final Cat cat;

  Dog(this.cat);

  String sayHi() {
    return this.cat.sound();
  }

  int jump() {
    return this.cat.walk();
  }
}

class MockCat extends Mock implements Cat {}

void main() {
  MockCat cat;
  Dog dog;

  @UnWrap()
  void setupCatSoundStub() {
    when(cat.sound()).thenReturn("Woof");
  }

  @UnWrap()
  void setupCatWalkstub() {
    when(cat.walk()).thenReturn(2);
  }

  @UnWrap()
  void expectCatCalled() {
    verify(cat.sound());
  }

  @UnWrap()
  void testDogWoof() {
    setupCatSoundStub();
    dog = Dog(cat);
    final sound = dog.sayHi();
    expect(sound, "Woof");
    expectCatCalled();
  }

  void expectCatWalked() {
    verify(cat.walk());
  }

  group('Dog Cat Play', () {
    setUp(() {
      cat = MockCat();
    });

    test('Dog woof', () {
      testDogWoof();
    });

    test('Dog woof then jump', () {
      testDogWoof();
      setupCatWalkstub();
      final steps = dog.jump();
      expect(steps, 2);
      expectCatWalked();
    });
  });
}

我想生成这样的代码

_$test.dart

void _$main() {
  MockCat cat;
  Dog dog;
  void expectCatWalked() {
    verify(cat.walk());
  }

  group('Dog Cat Play', () {
    setUp(() {
      cat = MockCat();
    });

    test('Dog woof', () {
      // testDogWoof();
      // setupCatSoundStub();
      when(cat.sound()).thenReturn("Woof");
      dog = Dog(cat);
      final sound = dog.sayHi();
      expect(sound, "Woof");
      // expectCatCalled();
      verify(cat.sound());
    });

    test('Dog woof then jump', () {
      // testDogWoof();
      // setupCatSoundStub();
      when(cat.sound()).thenReturn("Woof");
      dog = Dog(cat);
      final sound = dog.sayHi();
      expect(sound, "Woof");
      // expectCatCalled();
      verify(cat.sound());
      // setupCatWalkstub();
      when(cat.walk()).thenReturn(2);
      final steps = dog.jump();
      expect(steps, 2);
      expectCatWalked();
    });
  });
}

我在网上找到了一些教程,但我可以找到有关将函数体转换为字符串的文档(有些像 JavaScript 的

Function.prototype.toString()
方法)我是代码生成新手,所以我尝试打印所有字段,但我找不到类似的内容。

import 'dart:async';

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';

class InfoGenerator extends Generator {
  @override
  FutureOr<String> generate(LibraryReader library, BuildStep buildStep) {
    var buffer = StringBuffer();

    // library.allElements.forEach((element) {
    //   buffer.writeln(
    //       '// ${element.displayName} - ${element.source.fullName} - ${element.declaration}');
    // });
    library.allElements.whereType<TopLevelVariableElement>().forEach((element) {
      buffer.writeln('/*');
      buffer.writeln(element.toString());
      buffer.writeln('*/');
      buffer.writeln(
          '// ${element.name} - ${element.kind.displayName} - ${element.declaration}');
    });

    return buffer.toString();
  }
}

我对注释也是新手,所以我只是做了这个

/// What to do here ?
class UnWrap {
  const UnWrap();
}

我正在尝试做的事情可能吗?

flutter dart code-generation build-runner
2个回答
1
投票

您尝试做的事情确实是可能的,但它需要一种更复杂的方法,使用分析器包来解析和操作 Dart 代码的抽象语法树(AST)。让我们一步步分解问题并创建解决方案。

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';

class UnWrapGenerator extends GeneratorForAnnotation<UnWrap> {
  @override
  String generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {
    if (element is FunctionElement) {
      final functionBody = _getFunctionBody(element);
      return '// Unwrapped ${element.name}:\n$functionBody\n';
    }
    return '';
  }

  String _getFunctionBody(FunctionElement element) {
    final result = element.session.getParsedLibraryByElement(element.library);
    if (result is ParsedLibraryResult) {
      final functionDeclaration = result.getElementDeclaration(element);
      if (functionDeclaration != null) {
        final functionNode = functionDeclaration.node as FunctionDeclaration;
        final body = functionNode.functionExpression.body;
        if (body is BlockFunctionBody) {
          return body.block.toSource();
        }
      }
    }
    return '// Unable to retrieve function body';
  }
}

Builder unwrapGenerator(BuilderOptions options) =>
    LibraryBuilder(UnWrapGenerator(), generatedExtension: '.unwrap.dart');

该生成器执行以下操作:

它扩展了 GeneratorForAnnotation,这意味着它只会处理用 @UnWrap() 注解的元素。 在generateForAnnotatedElement方法中,它检查带注释的元素是否是一个函数。 如果它是一个函数,它会调用 _getFunctionBody 来检索函数的主体。 _getFunctionBody 方法使用分析器解析库并找到函数声明,然后将函数体提取为字符串。

现在,您需要设置 build.yaml 文件才能使用此生成器:

targets:
  $default:
    builders:
      your_package_name|unwrap:
        enabled: true

builders:
  unwrap:
    import: "package:your_package_name/builder.dart"
    builder_factories: ["unwrapGenerator"]
    build_extensions: {".dart": [".unwrap.dart"]}
    auto_apply: dependents
    build_to: source

您的包中还需要一个 builder.dart 文件:

library unwrap_builder;

导入'package:build/build.dart'; 导入'src/unwrap_generator.dart';

Builder unwrapGenerator(BuilderOptions 选项) => LibraryBuilder(UnWrapGenerator(), generatedExtension: '.unwrap.dart');

通过此设置,当您运行 pub run build_runner build 时,它将为每个包含 @UnWrap() 注释的文件生成一个新文件。生成的文件将包含解包的函数体。 要在测试中使用这些生成的函数,您可以导入生成的文件并直接调用解包的函数。以下是修改测试文件的方法:

import 'package:example_usage/src/unwrap.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'test.unwrap.dart'; // Import the generated file

class Cat {
  String sound() => "Meow";
  int walk() => 4;
}

class Dog {
  final Cat cat;

  Dog(this.cat);

  String sayHi() {
    return this.cat.sound();
  }

  int jump() {
    return this.cat.walk();
  }
}

class MockCat extends Mock implements Cat {}

void main() {
  MockCat cat;
  Dog dog;

  void expectCatWalked() {
    verify(cat.walk());
  }

  group('Dog Cat Play', () {
    setUp(() {
      cat = MockCat();
    });

    test('Dog woof', () {
      // Use the generated unwrapped functions
      setupCatSoundStub(cat);
      dog = Dog(cat);
      final sound = dog.sayHi();
      expect(sound, "Woof");
      expectCatCalled(cat);
    });

    test('Dog woof then jump', () {
      setupCatSoundStub(cat);
      dog = Dog(cat);
      final sound = dog.sayHi();
      expect(sound, "Woof");
      expectCatCalled(cat);
      setupCatWalkstub(cat);
      final steps = dog.jump();
      expect(steps, 2);
      expectCatWalked();
    });
  });
}

这种方法允许您保持测试的可读性和可维护性,同时在需要时仍然可以访问未包装的函数体。生成的文件将包含每个未包装函数的完整实现,从而可以轻松准确地查看每个测试用例中发生的情况。 每当您更改 @UnWrap() 带注释的函数时,请记住运行 pub run build_runner build 以重新生成未包装的版本。


0
投票

当我在 2021 年第一次发布这篇文章时,我也遇到了同样的情况——努力将函数体提取为

String

冒险进入代码生成器领域并不适合胆小的人。这是一项具有挑战性的工作,比使用

GetX
等镀银工具管理状态或处理
Flutter's
导航的复杂性要复杂得多。它使得驯服
MediaQuery
或与
Flexible
Expanded
小部件战斗看起来就像在公园散步一样轻松。

因此,我在这里分享一个单一的工作项目,值得花费数小时的努力和无数的挣扎,以帮助任何人。

https://github.com/tolotrasamuel/func_unwrapper

您正在寻找的是https://github.com/tolotrasamuel/func_unwrapper/blob/e94acf925741d57bfcf49bc4ebe933be1e1795b2/my_generators/lib/src/function_unwrap.dart#L262

  Block? getFuncBodyFromFuncDeclaration(FunctionDeclaration astNode) {
    final funcExpression =
        astNode.childEntities.whereType<FunctionExpression>().firstOrNull;
    final blockFuncBody = funcExpression?.childEntities
        .whereType<BlockFunctionBody>()
        .firstOrNull;
    final block = blockFuncBody?.childEntities.whereType<Block>().firstOrNull;
    // final blockExpression = block.childEntities.whereType<ExpressionStatement>().first;
    return block;
  }

可以直接使用,

Block
类有一个函数或者getter可以直接获取函数体,如
String
。但或者,它还包含函数的第一个和最后一个字符相对于原始源文件的偏移量。

  static Selector getBlockSelector(Block withElementWithOriginalOffset) {
    final statements = withElementWithOriginalOffset.childEntities
        .where((e) => (e is AstNode));
    if (statements.isEmpty) return Selector(0, 0);
    return Selector(statements.first.offset, statements.last.end);
  }

查看这一行以了解如何将原始源文件读取为字符串。

https://github.com/tolotrasamuel/func_unwrapper/blob/e94acf925741d57bfcf49bc4ebe933be1e1795b2/my_generators/lib/src/function_unwrap.dart#L74C1-L84C1

我更喜欢使用第二个选项,因为它更容易达到我的目的。

  Future<String?> readAsString(AssetId inputId) async {
    try {
      return await buildStep.readAsString(inputId);
    } catch (e, trace) {
      print('Possible BuildStepCompletedException $e');
      print(trace);
      return null;
    }
  }

祝你好运!

© www.soinside.com 2019 - 2024. All rights reserved.