From 1f18affc3093d22e9d4f0affbb9d17c6e9c176b9 Mon Sep 17 00:00:00 2001 From: Sebastian Thomschke Date: Mon, 27 Feb 2023 06:44:18 +0100 Subject: [PATCH] Improve support for abstracts (#305) --- .github/workflows/main.yml | 21 +- haxe_libraries/hscript.hxml | 5 + haxe_libraries/utest.hxml | 5 + src/dox/Processor.hx | 205 +++++++++++++++--- test/dox/{ => sample}/DoxTest.hx | 4 +- .../{ => sample}/emptyPackage/HiddenClass.hx | 2 +- test/dox/test/TestRunner.hx | 14 ++ test/dox/test/processor/ProcessorTest.hx | 80 +++++++ test/dox/test/processor/TestAbstract.hx | 91 ++++++++ tests.hxml | 10 + xml.hxml | 2 +- 11 files changed, 401 insertions(+), 38 deletions(-) create mode 100644 haxe_libraries/hscript.hxml create mode 100644 haxe_libraries/utest.hxml rename test/dox/{ => sample}/DoxTest.hx (99%) rename test/dox/{ => sample}/emptyPackage/HiddenClass.hx (50%) create mode 100644 test/dox/test/TestRunner.hx create mode 100644 test/dox/test/processor/ProcessorTest.hx create mode 100644 test/dox/test/processor/TestAbstract.hx create mode 100644 tests.hxml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 84287f0c..27961808 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,13 +42,16 @@ jobs: - run: npm install - - name: Generate XMLs + - name: Run unit tests + run: npx haxe tests.hxml + + - name: Test generation of XML files run: npx haxe xml.hxml - - name: Test [eval] + - name: Test cli [eval] run: npx haxe -D eval-stack runBase.hxml --run dox.Dox --help - - name: Test [neko] + - name: Test cli [neko] run: | set -eux npx haxe runBase.hxml -neko run.n @@ -59,7 +62,7 @@ jobs: with: python-version: 3.11 - - name: Test [python] + - name: Test cli [python] run: | set -eux npx haxe runBase.hxml -python bin/dox.py @@ -73,7 +76,7 @@ jobs: php-version: 7.4 extensions: mbstring, xml - - name: Test [php] + - name: Test cli [php] run: | set -eux npx haxe runBase.hxml -php bin/dox @@ -85,19 +88,19 @@ jobs: distribution: 'temurin' java-version: 11 - - name: Test [java] + - name: Test cli [java] run: | set -eux npx haxe runBase.hxml -java bin/java java -jar bin/java/Dox.jar - - name: Test [jvm] + - name: Test cli [jvm] run: | set -eux npx haxe runBase.hxml -java bin/jvm java -jar bin/jvm/Dox.jar - - name: Test [node] + - name: Test cli [node] run: | set -eux npx haxe runBase.hxml -lib hxnodejs -js bin/dox.js @@ -118,7 +121,7 @@ jobs: popd npx lix dev hxcpp $(npx haxelib config)/hxcpp/git - - name: Test [cpp] + - name: Test cli [cpp] run: | set -eux npx haxe runBase.hxml -cpp bin/cpp -D HXCPP_SILENT diff --git a/haxe_libraries/hscript.hxml b/haxe_libraries/hscript.hxml new file mode 100644 index 00000000..106e1f88 --- /dev/null +++ b/haxe_libraries/hscript.hxml @@ -0,0 +1,5 @@ +# @install: lix --silent download "haxelib:/hscript#2.5.0" into hscript/2.5.0/haxelib +# @run: haxelib run-dir hscript "${HAXE_LIBCACHE}/hscript/2.5.0/haxelib" +-cp ${HAXE_LIBCACHE}/hscript/2.5.0/haxelib/ +-D hscript=2.5.0 +--macro keep('IntIterator') \ No newline at end of file diff --git a/haxe_libraries/utest.hxml b/haxe_libraries/utest.hxml new file mode 100644 index 00000000..7c206205 --- /dev/null +++ b/haxe_libraries/utest.hxml @@ -0,0 +1,5 @@ +# @install: lix --silent download "haxelib:/utest#1.13.2" into utest/1.13.2/haxelib +-cp ${HAXE_LIBCACHE}/utest/1.13.2/haxelib/src +-D utest=1.13.2 +--macro utest.utils.Macro.checkHaxe() +--macro utest.utils.Macro.importEnvSettings() diff --git a/src/dox/Processor.hx b/src/dox/Processor.hx index a686321e..fe795f3c 100644 --- a/src/dox/Processor.hx +++ b/src/dox/Processor.hx @@ -3,6 +3,7 @@ package dox; import haxe.Serializer; import haxe.rtti.CType; +@:allow(dox.test) class Processor { public var infos:Infos; @@ -51,25 +52,25 @@ class Processor { throw 'Could not find toplevel package ${config.toplevelPackage}'; } } - function filter(root, tree):Void { + function filter(parent:Array, tree:TypeTree):Void { return switch (tree) { case TPackage(name, full, subs): var acc = []; subs.iter(filter.bind(acc)); if (acc.length > 0 && !isPathFiltered(full)) { - root.push(TPackage(name, full, acc)); + parent.push(TPackage(name, full, acc)); } case TClassdecl(t): t.fields = filterFields(t.fields); t.statics = filterFields(t.statics); if (!isTypeFiltered(t)) { - root.push(tree); + parent.push(tree); infos.addType(t.path, t); } case TEnumdecl(t): if (!isTypeFiltered(t)) { t.constructors = filterEnumFields(t.constructors); - root.push(tree); + parent.push(tree); infos.addType(t.path, t); } case TTypedecl(t): @@ -79,33 +80,15 @@ class Processor { t.type = CAnonymous(filterFields(fields)); default: } - root.push(tree); + parent.push(tree); infos.addType(t.path, t); } case TAbstractdecl(t): if (t.impl != null) { - var fields = new Array(); - var statics = new Array(); - t.impl.statics.iter(function(cf) { - if (hasMeta(cf.meta, ":impl")) { - if (cf.name == "_new") - cf.name = "new"; - else - switch (cf.type) { - case CFunction(args, _): - args.shift(); - case _: - } - fields.push(cf); - } else { - statics.push(cf); - } - }); - t.impl.fields = filterFields(fields); - t.impl.statics = filterFields(statics); + populateFieldsOfAbstract(t, root); } if (!isTypeFiltered(t)) { - root.push(tree); + parent.push(tree); infos.addType(t.path, t); } } @@ -114,6 +97,143 @@ class Processor { return newRoot; } + function populateFieldsOfAbstract(theAbstract:Abstractdef, root:TypeRoot) { + if (hasDoxMetadata(theAbstract.impl.meta, "is-populated")) { + return; // nothing to do + } + + var statics = new Array(); + var fields = new Array(); + + // collect direct members + for (cf in theAbstract.impl.statics) { + switch (cf.type) { + // handling functions + case CFunction(args, _): + if (cf.name == "_new") { // constructor + cf.name = "new"; + // the Haxe compiler automatically adds a ":noCompletion" + // so we remove the first auto-generated occurrence + var noCompletionMeta = cf.meta.find(m -> m.name == ":noCompletion"); + if (noCompletionMeta != null) cf.meta.remove(noCompletionMeta); + fields.push(cf); + } else if (args.length == 0 || args[0].name != "this") { + statics.push(cf); + } else + fields.push(cf); + // handling variables (declared with get and/or set accessor) + case CAbstract(name, params): + var isStatic = true; + switch(cf.get) { + case RCall("accessor"): + final accessor = theAbstract.impl.statics.find(f -> f.name == "get_" + cf.name); + if (accessor != null) { + switch (accessor.type) { + case CFunction(args, _): + if (args.length > 0 && args[0].name == "this") + isStatic = false; + case _: + } + } + case _: + switch(cf.set) { + case RCall("accessor"): + final accessor = theAbstract.impl.statics.find(f -> f.name == "set_" + cf.name); + if (accessor != null) { + switch (accessor.type) { + case CFunction(args, _): + if (args.length > 0 && args[0].name == "this") + isStatic = false; + case _: + } + } + case _: + } + } + if (isStatic) + statics.push(cf); + else { + fields.push(cf); + } + case _: + statics.push(cf); + } + } + + // collect forwarded static fields + final forwardStaticsMeta = findMeta(theAbstract.meta, ":forwardStatics"); + if (forwardStaticsMeta != null) { + switch (theAbstract.athis) { + case CClass(name, params): + switch (findInTrees(name, root)) { + case TClassdecl(realType): + if (forwardStaticsMeta.params == null || forwardStaticsMeta.params.length == 0) { + statics = statics.concat(realType.statics); + } else { + for (classStatic in realType.statics) { + if (forwardStaticsMeta.params.contains(classStatic.name)) + statics.push(classStatic); + } + } + case _: + } + case CAbstract(name, _): + switch (findInTrees(name, root)) { + case TAbstractdecl(realType): + populateFieldsOfAbstract(realType, root); + if (forwardStaticsMeta.params == null || forwardStaticsMeta.params.length == 0) { + statics = statics.concat(realType.impl.statics); + } else { + for (classStatic in realType.impl.statics) { + if (forwardStaticsMeta.params.contains(classStatic.name)) + statics.push(classStatic); + } + } + case _: + } + case _: + } + } + + // collect forwarded instance fields + final forwardMeta = findMeta(theAbstract.meta, ":forward"); + if (forwardMeta != null) { + switch (theAbstract.athis) { + case CClass(name, _): + switch (findInTrees(name, root)) { + case TClassdecl(realType): + if (forwardMeta.params == null || forwardMeta.params.length == 0) { + fields = fields.concat(realType.fields); + } else { + for (classField in realType.fields) { + if (forwardMeta.params.contains(classField.name)) + fields.push(classField); + } + } + case _: + } + case CAbstract(name, _): + switch (findInTrees(name, root)) { + case TAbstractdecl(realType): + populateFieldsOfAbstract(realType, root); + if (forwardMeta.params == null || forwardMeta.params.length == 0) { + fields = fields.concat(realType.impl.fields); + } else { + for (classField in realType.impl.fields) { + if (forwardMeta.params.contains(classField.name)) + fields.push(classField); + } + } + case _: + } + case _: + } + } + theAbstract.impl.statics = filterFields(statics); + theAbstract.impl.fields = filterFields(fields); + setMetaParam(theAbstract.impl.meta, ":dox", "is-populated"); + } + function filterFields(fields:Array) { return fields.filter(function(cf) { if (cf.overloads != null) { @@ -129,6 +249,29 @@ class Processor { return fields.filter(ef -> !hasHideMetadata(ef.meta) || hasShowMetadata(ef.meta)); } + /** Searches for a TClassdecl or TAbstractdecl in the given trees */ + static function findInTrees(path:String, trees:Array):Null { + for (tree in trees) { + final result = findInTree(path, tree); + if (result != null) return result; + } + return null; + } + + /** Searches for a TClassdecl or TAbstractdecl in the given tree */ + static function findInTree(path:String, tree:TypeTree):Null { + switch (tree) { + case TPackage(_, full, subs): + return findInTrees(path, subs); + case TClassdecl(t): + if (t.path == path) return tree; + case TAbstractdecl(t): + if (t.path == path) return tree; + case _: return null; + } + return null; + } + function sort(root:TypeRoot) { function getName(t:TypeTree) { return switch (t) { @@ -355,6 +498,10 @@ class Processor { return hasInclusionFilter; } + function findMeta(meta:MetaData, name:String):Null<{name:String, params:Array}> { + return meta.find(meta -> meta.name == name); + } + function hasMeta(meta:MetaData, name:String) { return meta.exists(meta -> meta.name == name); } @@ -370,4 +517,12 @@ class Processor { function hasHideMetadata(meta:MetaData):Bool { return hasDoxMetadata(meta, "hide") || hasMeta(meta, ":compilerGenerated") || hasMeta(meta, ":noCompletion"); } + + function setMetaParam(meta:MetaData, name:String, param:String) { + var doxMeta = findMeta(meta, name); + if (doxMeta == null) + meta.push({name: name, params: [param]}); + else if (!doxMeta.params.contains(param)) + doxMeta.params.push(param); + } } diff --git a/test/dox/DoxTest.hx b/test/dox/sample/DoxTest.hx similarity index 99% rename from test/dox/DoxTest.hx rename to test/dox/sample/DoxTest.hx index 43d2b2ff..553a86b3 100644 --- a/test/dox/DoxTest.hx +++ b/test/dox/sample/DoxTest.hx @@ -1,4 +1,4 @@ -package dox; +package dox.sample; /** *

A node in the entity hierarchy, and a collection of components.

@@ -399,7 +399,7 @@ typedef PlatformConditionalized = #if cpp { } #elseif cs(a:String, b:Int) -> Void #elseif neko Array #else {} #end; /** - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit **/ diff --git a/test/dox/emptyPackage/HiddenClass.hx b/test/dox/sample/emptyPackage/HiddenClass.hx similarity index 50% rename from test/dox/emptyPackage/HiddenClass.hx rename to test/dox/sample/emptyPackage/HiddenClass.hx index 64a3196f..a2a9ac0b 100644 --- a/test/dox/emptyPackage/HiddenClass.hx +++ b/test/dox/sample/emptyPackage/HiddenClass.hx @@ -1,4 +1,4 @@ -package dox.emptyPackage; +package dox.sample.emptyPackage; @:dox(hide) class HiddenClass {} diff --git a/test/dox/test/TestRunner.hx b/test/dox/test/TestRunner.hx new file mode 100644 index 00000000..0018b601 --- /dev/null +++ b/test/dox/test/TestRunner.hx @@ -0,0 +1,14 @@ +package dox.test; + +import dox.test.processor.ProcessorTest; +import utest.ui.Report; +import utest.Runner; + +class TestRunner { + public static function main():Void { + var runner = new Runner(); + runner.addCase(new ProcessorTest()); + Report.create(runner); + runner.run(); + } +} diff --git a/test/dox/test/processor/ProcessorTest.hx b/test/dox/test/processor/ProcessorTest.hx new file mode 100644 index 00000000..f1cd1718 --- /dev/null +++ b/test/dox/test/processor/ProcessorTest.hx @@ -0,0 +1,80 @@ +package dox.test.processor; + +import utest.Assert; + +using Lambda; +using StringTools; + +class ProcessorTest extends utest.Test { + public function testProcessor() { + var cfg = new Config(Sys.getCwd()); + cfg.inputPath = "bin/doc.xml"; + + cfg.outputPath = "bin/pages"; + cfg.toplevelPackage = "dox.test"; + cfg.loadTheme("default"); + + var xml = try Xml.parse(sys.io.File.getContent("bin/doc.xml")).firstElement() catch (err:Dynamic) throw err; + var parser = new haxe.rtti.XmlParser(); + + parser.process(xml, "unused"); + + var root = new Processor(cfg).process(parser.root); + var testAbstract = Processor.findInTrees("dox.test.processor.TestAbstract", root); + Assert.notNull(testAbstract); + switch (testAbstract) { + case TAbstractdecl(realType): + var fields = realType.impl.fields.map(cf -> cf.name); + fields.sort(Reflect.compare); + var statics = realType.impl.statics.map(cf -> cf.name); + statics.sort(Reflect.compare); + Assert.same([ + "abstract_instance_func_no_args", + "abstract_instance_func_with_args", + "abstract_instance_ro_var", + "abstract_instance_wo_var", + "impl_instance_func", + "impl_instance_var", + "new" + ], fields); + Assert.same([ + "abstract_static_func_no_args", + "abstract_static_func_with_args", + "abstract_static_ro_var", + "abstract_static_wo_var", + "impl_static_func", + "impl_static_var" + ], statics); + case _: + throw "Type TestAbstract is not an abstract!"; + } + var testAbstractOfAbstract = Processor.findInTrees("dox.test.processor.TestAbstractOfAbstract", root); + Assert.notNull(testAbstractOfAbstract); + switch (testAbstractOfAbstract) { + case TAbstractdecl(realType): + var fields = realType.impl.fields.map(cf -> cf.name); + fields.sort(Reflect.compare); + var statics = realType.impl.statics.map(cf -> cf.name); + statics.sort(Reflect.compare); + Assert.same([ + "abstract_instance_func_no_args", + "abstract_instance_func_with_args", + "abstract_instance_ro_var", + "abstract_instance_wo_var", + "impl_instance_func", + "impl_instance_var", + "new" + ], fields); + Assert.same([ + "abstract_static_func_no_args", + "abstract_static_func_with_args", + "abstract_static_ro_var", + "abstract_static_wo_var", + "impl_static_func", + "impl_static_var" + ], statics); + case _: + throw "Type TestAbstractOfAbstract is not an abstract!"; + } + } +} diff --git a/test/dox/test/processor/TestAbstract.hx b/test/dox/test/processor/TestAbstract.hx new file mode 100644 index 00000000..bb342a32 --- /dev/null +++ b/test/dox/test/processor/TestAbstract.hx @@ -0,0 +1,91 @@ +package dox.test.processor; + +@:keep +@:forward +@:forwardStatics +abstract TestAbstractOfAbstract(TestAbstract) from TestAbstract to TestAbstract {} + +@:keep +@:forward(impl_instance_var, impl_instance_func) +@:forwardStatics(impl_static_var, impl_static_func) +abstract TestAbstract(TestAbstractImpl) from TestAbstractImpl to TestAbstractImpl { + /** + * static members + */ + public static var abstract_static_ro_var(get, never):TestAbstract; + + private static function get_abstract_static_ro_var():TestAbstract { + return null; + } + + public static var abstract_static_wo_var(never, set):TestAbstract; + + private static function set_abstract_static_wo_var(value:TestAbstract):TestAbstract { + return value; + } + + public static function abstract_static_func_no_args():Void {}; + + public static function abstract_static_func_with_args(unused:Int):Void {}; + + /** + * instance members + */ + public function new(someValue:Int) { + this = new TestAbstractImpl(); + } + + public var abstract_instance_ro_var(get, never):TestAbstract; + + private function get_abstract_instance_ro_var():TestAbstract { + return this; + }; + + public var abstract_instance_wo_var(never, set):TestAbstract; + + private function set_abstract_instance_wo_var(value:TestAbstract):TestAbstract { + return value; + }; + + public function abstract_instance_func_no_args():Void {}; + + public function abstract_instance_func_with_args(unused:Int):Void {}; +} + +@:keep +private class TestAbstractImpl { + /* + * fields that should be forwarded + */ + public static var impl_static_var:Int = 10; + + public static function impl_static_func():Void {}; + + public var impl_instance_var:Int; + + public function impl_instance_func():Void {}; + + /* + * public fields that should not be forwarded + */ + public static var hidden_impl_static_var:Int; + + public static function hidden_impl_static_func():Void {}; + + public var hidden_impl_instance_var:Int; + + public function hidden_impl_instance_func():Void {}; + + /* + * private fields that should not be forwarded + */ + private static var private_impl_static_var:Int; + + private static function private_impl_static_func():Void {}; + + private var private_impl_instance_var:Int; + + private function private_impl_instance_func():Void {}; + + public function new() {} +} diff --git a/tests.hxml b/tests.hxml new file mode 100644 index 00000000..ff1b6a59 --- /dev/null +++ b/tests.hxml @@ -0,0 +1,10 @@ +-lib hxtemplo +-lib hxparse +-lib hxargs +-lib markdown +-lib utest +-cp src +-cp test +--macro include('dox.test') +-xml bin/doc.xml +--run dox.test.TestRunner diff --git a/xml.hxml b/xml.hxml index a1bbab00..dba732e3 100644 --- a/xml.hxml +++ b/xml.hxml @@ -1,6 +1,6 @@ --no-output -cp test -dox +dox.sample -D doc-gen --each