Search code examples
d

D: finding all functions with certain attribute


Is it currently possible to scan/query/iterate all functions (or classes) with some attribute across modules?

For example:


source/packageA/something.d:

@sillyWalk(10)
void doSomething()
{
}

source/packageB/anotherThing.d:

@sillyWalk(50)
void anotherThing()
{
}

source/main.d:

void main()
{
    for (func; /* All @sillWalk ... */) {
        ...
    }
}

Solution

  • Believe it or not, but yes, it kinda is... though it is REALLY hacky and has a lot of holes. Code: http://arsdnet.net/d-walk/

    Running that will print:

    Processing: module main
    Processing: module object
    Processing: module c
    Processing: module attr
    test2() has sillyWalk
    main() has sillyWalk
    

    You'll want to take a quick look at c.d, b.d, and main.d to see the usage. The onEach function in main.d processes each hit the helper function finds, here just printing the name. In the main function, you'll see a crazy looking mixin(__MODULE__) - this is a hacky trick to get a reference to the current module as a starting point for our iteration.

    Also notice that the main.d file has a module project.main; line up top - if the module name was just main as it is automatically without that declaration, the mixin hack would confuse the module for the function main. This code is really brittle!

    Now, direct your attention to attr.d: http://arsdnet.net/d-walk/attr.d

    module attr;
    
    struct sillyWalk { int i; }
    
    enum isSillyWalk(alias T) = is(typeof(T) == sillyWalk);
    
    import std.typetuple;
    alias hasSillyWalk(alias what) = anySatisfy!(isSillyWalk, __traits(getAttributes, what));
    enum hasSillyWalk(what) = false;
    
    alias helper(alias T) = T;
    alias helper(T) = T;
    
    void allWithSillyWalk(alias a, alias onEach)() {
        pragma(msg, "Processing: " ~ a.stringof);
        foreach(memberName; __traits(allMembers, a)) {
            // guards against errors from trying to access private stuff etc.
            static if(__traits(compiles, __traits(getMember, a, memberName))) {
                alias member = helper!(__traits(getMember, a, memberName));
    
                // pragma(msg, "looking at " ~ memberName);
                import std.string;
                static if(!is(typeof(member)) && member.stringof.startsWith("module ")) {
                    enum mn = member.stringof["module ".length .. $];
                    mixin("import " ~ mn ~ ";");
                    allWithSillyWalk!(mixin(mn), onEach);
                }
    
                static if(hasSillyWalk!(member)) {
                    onEach!member;
                }
            }
        }
    }
    

    First, we have the attribute definition and some helpers to detect its presence. If you've used UDAs before, nothing really new here - just scanning the attributes tuple for the type we're interested in.

    The helper templates are a trick to abbreviate repeated calls to __traits(getMember) - it just aliases it to a nicer name while avoiding a silly parse error in the compiler.

    Finally, we have the meat of the walker. It loops over allMembers, D's compile time reflection's workhorse (if you aren't familiar with this, take a gander at the sample chapter of my D Cookbook https://www.packtpub.com/application-development/d-cookbook - the "Free Sample" link is the chapter on compile time reflection)

    Next, the first static if just makes sure we can actually get the member we want to get. Without that, it would throw errors on trying to get private members of the automatically imported object module.

    The end of the function is simple too - it just calls our onEach thing on each element. But the middle is where the magic is: if it detects a module (sooo hacky btw but only way I know to do it) import in the walk, it imports it here, gaining access to it via the mixin(module) trick used at the top level... thus recursing through the program's import graph.

    If you play around, you'll see it actually kinda works. (Compile all those files together on the command line btw for best results: dmd main.d attr.d b.d c.d)

    But it also has a number of limitations:

    • Going into class/struct members is possible, but not implemented here. Pretty straightforward though: if the member is a class, just descend into it recursively too.

    • It is liable to break if a module shares a name with a member, such as the example with main mentioned above. Work around by using unique module names with some package dots too, should be ok.

    • It will not descend into function-local imports, meaning it is possible to use a function in the program that will not be picked up by this trick. I'm not aware of any solution to this in D today, not even if you're willing to use every hack in the language.

    • Adding code with UDAs is always tricky, but doubly so here because the onEach is a function with its on scope. You could perhaps build up a global associative array of delegates into handlers for the things though: void delegate()[string] handlers; /* ... */ handlers[memberName] = &localHandlerForThis; kind of thing for runtime access to the information.

    • I betcha it will fail to compile on more complex stuff too, I just slapped this together now as a toy proof of concept.

    Most D code, instead of trying to walk the import tree like this, just demands that you mixin UdaHandler!T; in the individual aggregate or module where it is used, e.g. mixin RegisterSerializableClass!MyClass; after each one. Maybe not super DRY, but way more reliable.

    edit: There's another bug I didn't notice when writing the answer originally: the "module b.d;" didn't actually get picked up. Renaming it to "module b;" works, but not when it includes the package.

    ooooh cuz it is considered "package mod" in stringof.... which has no members. Maybe if the compiler just called it "module foo.bar" instead of "package foo" we'd be in business though. (of course this isn't practical for application writers... which kinda ruins the trick's usefulness at this time)