Validator.hx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #if macro
  2. import validation.Lines;
  3. import validation.ValidationReport;
  4. import validation.ValidationError;
  5. import validation.Exception;
  6. import haxe.display.Position.Location;
  7. import haxe.macro.Context;
  8. import haxe.macro.Expr;
  9. import haxe.macro.Compiler;
  10. import validation.Target;
  11. import haxe.io.Path;
  12. using sys.io.File;
  13. using haxe.macro.Tools;
  14. using StringTools;
  15. #end
  16. class Validator {
  17. macro static public function expect(hxExpr:Expr, output:Expr):Expr {
  18. var outMap = try {
  19. parseExpectedOutput(output);
  20. } catch(e:ExpectedOutputException) {
  21. Context.error(e.msg, e.pos);
  22. return macro {};
  23. }
  24. expected.push({pos:hxExpr.pos, out:outMap});
  25. if(!onAfterGenerateAdded) {
  26. onAfterGenerateAdded = true;
  27. Context.onAfterGenerate(verifyExpectations);
  28. }
  29. return hxExpr;
  30. }
  31. #if macro
  32. static var onAfterGenerateAdded = false;
  33. static var expected = new Array<{pos:Position, out:Map<Target,String>}>();
  34. static var sourceMapsCache = new Map<String,SourceMap>();
  35. static function verifyExpectations() {
  36. var target = try {
  37. getCurrentTarget();
  38. } catch(e:InvalidTargetException) {
  39. Context.error(e.msg, e.pos);
  40. return;
  41. }
  42. var errors = [];
  43. for(expectation in expected) {
  44. var expectedCode = expectation.out.get(target);
  45. var location = expectation.pos.toLocation();
  46. var hxPos = location.range.start;
  47. var generated = getGeneratedContent(target, location);
  48. var found = false;
  49. //look for the mappings generated from the `hxPos`
  50. generated.map.eachMapping(genPos -> {
  51. if(found) return;
  52. if(genPos.originalLine == hxPos.line && genPos.originalColumn + 1 == hxPos.character) {
  53. var code = generated.code[genPos.generatedLine].substr(genPos.generatedColumn);
  54. //compare with expected output ignoring leading whitespaces
  55. if(code.trim().startsWith(expectedCode)) {
  56. found = true;
  57. }
  58. }
  59. });
  60. if(!found) {
  61. errors.push({
  62. expected: expectedCode,
  63. pos: {
  64. file: location.file,
  65. line: hxPos.line,
  66. column: hxPos.character
  67. }
  68. });
  69. }
  70. }
  71. Sys.println(haxe.Json.stringify(buildValidationReport(errors), ' '));
  72. }
  73. static function buildValidationReport(errors:Array<ValidationError>):ValidationReport {
  74. return {
  75. success: errors.length == 0,
  76. summary: {
  77. assertions: expected.length,
  78. failures: errors.length
  79. },
  80. errors: errors
  81. }
  82. }
  83. static function getCurrentTarget():Target {
  84. return if(Context.defined('js')) {
  85. Js;
  86. } else if(Context.defined('php')) {
  87. Php;
  88. } else {
  89. throw new InvalidTargetException('Current target is not supported', (macro 0).pos);
  90. }
  91. }
  92. static function getGeneratedContent(target:Target, location:Location):{code:Lines, map:SourceMap} {
  93. var sourceFile;
  94. var mapFile;
  95. switch(target) {
  96. case Js:
  97. sourceFile = Compiler.getOutput();
  98. mapFile = '${Compiler.getOutput()}.map';
  99. case Php:
  100. var phpFilePath = new Path(location.file.toString().substr(location.file.toString().indexOf('cases')));
  101. phpFilePath.ext = 'php';
  102. sourceFile = Path.join([Compiler.getOutput(), 'lib', phpFilePath.toString()]);
  103. mapFile = '$sourceFile.map';
  104. }
  105. return {
  106. code: sourceFile.getContent().split('\n'),
  107. map: getSourceMap(mapFile)
  108. }
  109. }
  110. static function getSourceMap(mapFile:String):SourceMap {
  111. var sourceMap = sourceMapsCache.get(mapFile);
  112. if(sourceMap == null) {
  113. sourceMap = new SourceMap(mapFile.getContent());
  114. sourceMapsCache.set(mapFile, sourceMap);
  115. }
  116. return sourceMap;
  117. }
  118. static function parseExpectedOutput(output:Expr):Null<Map<Target,String>> {
  119. var result = new Map();
  120. switch(output) {
  121. case macro $a{exprs}:
  122. var expectedTargets = Target.getConstructors();
  123. for(expr in exprs) {
  124. switch(expr) {
  125. case macro $target => ${{expr:EConst(CString(outStr))}}:
  126. var target = switch(target) {
  127. case macro Js: Js;
  128. case macro Php: Php;
  129. case _: throw new ExpectedOutputException('Invalid target in expected output: ${target.toString()}', target.pos);
  130. }
  131. expectedTargets.remove(target.getName());
  132. result.set(target, outStr);
  133. case _:
  134. throw new ExpectedOutputException('Invalid item in map declaration for expected output. Should be `Target => "generated_code"`', expr.pos);
  135. }
  136. }
  137. if(expectedTargets.length > 0) {
  138. throw new ExpectedOutputException('Missing expected code for targets: ${expectedTargets.join(', ')}.', output.pos);
  139. }
  140. case _:
  141. throw new ExpectedOutputException('The second argument for "expect" should be a map declaration.', output.pos);
  142. }
  143. return result;
  144. }
  145. #end
  146. }