Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get function body as string using source_gen and build_runner?

My goal is to make my unit tests easy to understand. Currently, they are hard to understand because they have so many nested functions.

I want to use build_runner to generate the code of the unit with all functions unwrapped.

So here is an example of my current test:

test.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();
    });
  });
}

I want to generate a code like this

_$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();
    });
  });
}

I found some tutorial online but I could find documentations about getting the function body into string ( some like JavaScript's Function.prototype.toString() method ) I am new to code generation so I tried to print all fields but I can't find anything like that.

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();
  }
}

I am also new to annotations so I just made this up

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

Is what I am trying to do even possible ?

like image 560
TSR Avatar asked Oct 16 '25 02:10

TSR


1 Answers

When I first posted this in 2021, I was in the same boat—struggling to extract the function body as a String.

Venturing into the realm of code generators is not for the faint of heart. It’s a challenging endeavor, far more complex than managing state with silver-plated tools like GetX or dealing with the intricacies of Flutter's navigation. It makes taming the MediaQuery or battling with Flexible and Expanded widgets look like a walk in the park.

So here I share in single working project, worth hours of effort and countless struggles, to help anyone.

https://github.com/tolotrasamuel/func_unwrapper

What you are looking for is 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;
  }

You can directly use it, the Block class has a function or a getter to get the function body directly as String. But alternatively, it also contains the offset of the first and last character of the function from the original source file.

  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);
  }

Check out this line to know how to read the original source file as String.

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

I preferred using the second option because it was easier for my purpose.

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

Good luck!

like image 146
TSR Avatar answered Oct 18 '25 17:10

TSR