back2dos / travix

Travis helper.
The Unlicense
33 stars 12 forks source link

Testing compilation errors #19

Closed ciscoheat closed 8 years ago

ciscoheat commented 8 years ago

In the immutable-hx library, I want to test if the compilation fails. I've done that through a special prefix in a test file: https://github.com/ciscoheat/immutable-hx/blob/master/tests/RunTests.hx

When testing, a small script is run that uncomments each line containing //TEST: one by one, the project is built, and if compilation fails, it is considered a passing test.

Would this be interesting to have in travix? If so, how to do it?

back2dos commented 8 years ago

Well, for one I'm not so sure the approach is really the best. If you have some 100 //TEST: lines you'll be doing a lot of compiling.

You could be doing something like this:

class Assert {
  macro public static function compileError(expr:Expr)
    return
      try {
        Context.typeof(expr);
        macro @:pos(expr.pos) assertTrue(false);
      }
      catch (e:Dynamic)
        macro @:pos(expr.pos) assertTrue(true);
}

I'll leave it to you to adapt it to buddy and produce more helpful error messages.

But in general you could just write stuff like:

Assert.compileError(path.to.ClassThatViolatesRules);
Assert.compilerError(immutableVar = newValue);

The second type of statement is where you need a little trick. Currently, on expression level you are using Context.error to reject invalid subexpressions. Instead, you could return an expression that - once typed - error.

class CompilerError {
  static public function errorExpr(original:Expr, message:String) {
    return macro @:pos(original.pos) CompilerError.raise($v{message});
  }
  static public macro function raise(error:String) {
    Context.error(error, Context.currentPos());
  }
}

That's a technique I sometimes use in tink (any time I can muster the discipline), because it allows the whole macro to run, as opposed to Context.error which will just abort a build-macro and thus skip all of its effects, often leading to a flood of errors in dependent classes.

So to answer your question: I think this would be better suited as an addition to buddy ;)

ciscoheat commented 8 years ago

Great, thanks, I'll add this to buddy for sure. :) Enjoy wwx!

ciscoheat commented 8 years ago

I've added this to buddy now, it works great for one part of the problem, but there are some tricky parts left. If you look at line 120 for example, that line itself will compile, but the actual failure will occur in a callback to Macro.onGenerate. How to test that?

Another case is a property test. If a property setter or default exists, I also want to fail compilation, but I can't use Context.typeof on that part of the code.

Any ideas how to solve this?

back2dos commented 8 years ago

Ok, hmm. I guess you have to do some extra work there :)

First, move the validation code out somewhere, to something like:

static function checkType(type:haxe.macro.Type):Some<Error>;

//and now, something rather domain-specific like this:

class Assert {
  macro static public function isInvalid(e:ExprOf<Class<Dynamic>>)
    return switch checkType(Context.getType(e.toString()) {
       case Some(_): macro @:pos(e.pos) assertTrue(true);
       case None: macro @:pos(e.pos) assertTrue(false);
    }
}

So that works should deal with that.

Also, I would move some of the work into the build macro, i.e. any immutable field should already be built with null or never write access. So from the outside, the compiler does the heavy lifting already. You only have to check the integrity of the class itself (and subclasses I guess).

ciscoheat commented 8 years ago

Thanks again! Since this is isn't travix related anymore, closing this.