diff --git a/.gitignore b/.gitignore index fd95a84d4acca7f2b894934f1481db93bfa9e028..9d4be9ee2db5696c31176d47d7f50f0d5eed5624 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.idea/ /*.iml +/out/ /src/gen/ /src/tmp/ diff --git a/build.gradle b/build.gradle index e81f083b42ee9ae2a4f8c000e404113ee65a293d..0e8178ecd1b87224ca220866fe3a75a8c93f25b4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,8 @@ plugins { id 'java' id 'maven' - id 'org.jastadd' version '1.12.0' + id 'groovy' + id 'org.jastadd' version '1.12.2' } if (!file('extendj/jastadd_modules').exists()) { @@ -33,8 +34,6 @@ jastadd { jastadd { basedir "src/main/jastadd" - include "**/*.ast" - include "**/*.jadd" include "**/*.jrag" } } @@ -57,8 +56,10 @@ repositories { } dependencies { - compile 'se.llbit:jo-json:1.3.0' + compile 'se.llbit:jo-json:1.3.1' compile 'org.extendj:trace:0.1' + + testCompile 'org.spockframework:spock-core:1.1-groovy-2.4' } def mainClassName = 'org.extendj.ragdoc.RagDocBuilder' diff --git a/src/main/java/org/extendj/ragdoc/AstDeclParser.java b/src/main/java/org/extendj/ragdoc/AstDeclParser.java new file mode 100644 index 0000000000000000000000000000000000000000..538d79b0a6a703c3c615155fe5c223853513643f --- /dev/null +++ b/src/main/java/org/extendj/ragdoc/AstDeclParser.java @@ -0,0 +1,194 @@ +/* Copyright (c) 2018, Jesper Öqvist <jesper.oqvist@cs.lth.se> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.extendj.ragdoc; + +import se.llbit.io.LookaheadReader; +import se.llbit.json.JsonArray; +import se.llbit.json.JsonObject; +import se.llbit.json.JsonParser; + +import java.io.Closeable; +import java.io.IOException; +import java.io.StringReader; + +public class AstDeclParser implements AutoCloseable, Closeable { + private static final int EOF = -1; + + private final LookaheadReader in; + + public AstDeclParser(String astdecl) { + in = new LookaheadReader(new StringReader(astdecl), 2); + } + + private void accept(char c) throws IOException, JsonParser.SyntaxError { + int next = in.pop(); + if (next == EOF) { + throw new JsonParser.SyntaxError(String.format("unexpected end of input (expected '%c')", c)); + } + if (next != c) { + throw new JsonParser.SyntaxError( + String.format("unexpected character (was '%c', expected '%c')", (char) next, c)); + } + } + + JsonObject parse() throws IOException, JsonParser.SyntaxError { + JsonObject decl = new JsonObject(); + String name = parseName(); + decl.add("n", name); + skipWhitespace(); + if (in.peek() == ':' && in.peek(1) != ':') { + accept(':'); + skipWhitespace(); + String extendsName = parseName(); + decl.add("e", extendsName); + skipWhitespace(); + } + if (in.peek() != ';') { + accept(':'); + accept(':'); + accept('='); + skipWhitespace(); + JsonArray components = new JsonArray(); + while (in.peek() != ';') { + components.add(parseComponent()); + skipWhitespace(); + } + if (!components.isEmpty()) { + decl.add("c", components); + } + } + accept(';'); + return decl; + } + + private JsonObject parseComponent() throws IOException, JsonParser.SyntaxError { + JsonObject component = new JsonObject(); + if (in.peek() == '[') { + accept('['); + skipWhitespace(); + String name = parseName(); + + skipWhitespace(); + if (in.peek() == ':') { + accept(':'); + skipWhitespace(); + String extendsName = parseName(); + component.add("n", name); + component.add("e", extendsName); + skipWhitespace(); + } else { + component.add("e", name); + } + + component.add("k", "opt"); + + skipWhitespace(); + accept(']'); + return component; + } else if (in.peek() == '<') { + accept('<'); + skipWhitespace(); + String name = parseName(); + component.add("n", name); + + skipWhitespace(); + if (in.peek() == ':') { + accept(':'); + skipWhitespace(); + String extendsName = parseName(); + component.add("e", extendsName); + skipWhitespace(); + } else { + component.add("e", "String"); + } + + component.add("k", "token"); + + skipWhitespace(); + accept('>'); + return component; + } else { + String name = parseName(); + + skipWhitespace(); + if (in.peek() == ':') { + accept(':'); + skipWhitespace(); + String extendsName = parseName(); + component.add("n", name); + component.add("e", extendsName); + skipWhitespace(); + } else { + component.add("e", name); + } + + if (in.peek() == '*') { + accept('*'); + component.add("k", "list"); + } + return component; + } + } + + private void skipWhitespace() throws IOException { + while (isWhitespace(in.peek())) { + in.pop(); + } + } + + private boolean isWhitespace(int chr) { + return chr == 0x20 || chr == 0x09 || chr == 0x0A || chr == 0x0D; + } + + private boolean isNameChar(int chr) { + return chr != EOF && (chr == '.' || Character.isLetterOrDigit(chr)); + } + + private String chrToStr(int chr) { + return chr != EOF ? String.format("%c", chr) : "EOF"; + } + + String parseName() throws IOException, JsonParser.SyntaxError { + int next = in.peek(); + if (!isNameChar(next)) { + throw new JsonParser.SyntaxError( + String.format("Error: expected name or typename, found %s", chrToStr(next))); + } + StringBuilder name = new StringBuilder(); + while (isNameChar(in.peek())) { + name.append((char) in.pop()); + } + return name.toString(); + } + + @Override public void close() throws IOException { + in.close(); + } +} diff --git a/src/main/java/org/extendj/ragdoc/JavaDocParser.java b/src/main/java/org/extendj/ragdoc/JavaDocParser.java index a1bbc2b5ed807358d559ed71925fc42cd0524ee4..5111ce1ce418bb9787688f8d83a14c89ff29ad73 100644 --- a/src/main/java/org/extendj/ragdoc/JavaDocParser.java +++ b/src/main/java/org/extendj/ragdoc/JavaDocParser.java @@ -233,7 +233,7 @@ public class JavaDocParser extends Object { private String parseInlineTag() { if (doc[i] != '{' || doc[i + 1] != '@') { - // no opening bracket + // No opening bracket. return ""; } int j = i + 2; diff --git a/src/main/java/org/extendj/ragdoc/JsonBuilder.java b/src/main/java/org/extendj/ragdoc/JsonBuilder.java index d5d8d8a68116a0f7392f731ddcff240c8044f7e6..34a16fe810a999c1d8ec0f56e2919048eec7fef2 100644 --- a/src/main/java/org/extendj/ragdoc/JsonBuilder.java +++ b/src/main/java/org/extendj/ragdoc/JsonBuilder.java @@ -46,9 +46,11 @@ import org.extendj.util.Sorting; import se.llbit.json.Json; import se.llbit.json.JsonArray; import se.llbit.json.JsonObject; +import se.llbit.json.JsonParser; import se.llbit.json.JsonValue; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -145,6 +147,12 @@ public class JsonBuilder { } } + /** + * Build a type reference object. + * + * <p>Type references point to a single type. Disambiguation is handled + * by using unique ID suffixes. + */ private JsonObject typeRef(TypeDecl type) { // TODO: handle wildcard types. JsonObject obj = new JsonObject(); @@ -307,11 +315,13 @@ public class JsonBuilder { if (!params.isEmpty()) { doc.add("params", params); } - doc.add("description", Json.of(javadoc)); + if (!javadoc.isEmpty()) { + doc.add("description", Json.of(javadoc)); + } if (aspectName != null) { aspects.add(aspectName); } - return doc; + return doc.isEmpty() ? null : doc; } return null; } @@ -377,6 +387,30 @@ public class JsonBuilder { } JsonObject doc = type.jsonDocObject(); if (doc != null) { + String astdecl = doc.get("astdecl").stringValue(""); + if (!astdecl.isEmpty()) { + // Parse ast declaration. + try (AstDeclParser parser = new AstDeclParser(astdecl)){ + // Create declaration structure. + JsonObject decl = parser.parse(); + if (!decl.get("e").stringValue("").isEmpty()) { + decl.set("e", typeRef(type.lookupType(decl.get("e").stringValue("")).singletonValue())); + } + doc.set("astdecl", decl); + JsonArray array = decl.get("c").array(); + for (JsonValue c : array) { + JsonObject comp = c.object(); + if (!comp.get("e").stringValue("").isEmpty()) { + comp.set("e", + typeRef(type.lookupType(comp.get("e").stringValue("")).singletonValue())); + } + } + } catch (IOException e) { + e.printStackTrace(); + } catch (JsonParser.SyntaxError syntaxError) { + syntaxError.printStackTrace(); + } + } obj.add("doc", doc); } addInheritedMembers(type, obj); diff --git a/src/test/groovy/org/extendj/ragdoc/AstDeclParserSpec.groovy b/src/test/groovy/org/extendj/ragdoc/AstDeclParserSpec.groovy new file mode 100644 index 0000000000000000000000000000000000000000..ec2694a7fe765f3f8f27faa19018499e01958b9d --- /dev/null +++ b/src/test/groovy/org/extendj/ragdoc/AstDeclParserSpec.groovy @@ -0,0 +1,167 @@ +/* Copyright (c) 2018, Jesper Öqvist <jesper.oqvist@cs.lth.se> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.extendj.ragdoc + +import spock.lang.* + +class AstDeclParserSpec extends Specification { + + def "No children (no extends)"() { + when: + def res + new AstDeclParser("Decl;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Decl"}' + } + + def "No children (extends)"() { + when: + def res + new AstDeclParser("IdDecl:Decl;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"IdDecl","e":"Decl"}' + } + + def "No children (extends) + whitespace"() { + when: + def res + new AstDeclParser("IdDecl : Decl ;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"IdDecl","e":"Decl"}' + } + + def "Binary declaration"() { + when: + def res + new AstDeclParser("Binary ::= Left:Expr Right:Expr;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Binary","c":[{"n":"Left","e":"Expr"},{"n":"Right","e":"Expr"}]}' + } + + def "List child"() { + when: + def res + new AstDeclParser("Program ::= CompilationUnit*;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Program","c":[{"e":"CompilationUnit","k":"list"}]}' + } + + def "Named List child"() { + when: + def res + new AstDeclParser("Program ::= Unit:CompilationUnit*;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Program","c":[{"n":"Unit","e":"CompilationUnit","k":"list"}]}' + } + + def "Named List child + whitespace"() { + when: + def res + new AstDeclParser("Program ::= Unit : CompilationUnit *;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Program","c":[{"n":"Unit","e":"CompilationUnit","k":"list"}]}' + } + + def "Opt child"() { + when: + def res + new AstDeclParser("Return ::= [Expr];").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Return","c":[{"e":"Expr","k":"opt"}]}' + } + + def "Named Opt child"() { + when: + def res + new AstDeclParser("Return ::= [Result:Expr];").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Return","c":[{"n":"Result","e":"Expr","k":"opt"}]}' + } + + def "Named Opt child + whitespace"() { + when: + def res + new AstDeclParser("Return ::= [ Result : Expr ] ;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Return","c":[{"n":"Result","e":"Expr","k":"opt"}]}' + } + + def "Token child"() { + when: + def res + new AstDeclParser("Number ::= <NUM>;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Number","c":[{"n":"NUM","e":"String","k":"token"}]}' + } + + def "Named Token child"() { + when: + def res + new AstDeclParser("Number ::= <NUM:Integer>;").withCloseable { parser -> + res = parser.parse() + } + + then: + res.toCompactString() == '{"n":"Number","c":[{"n":"NUM","e":"Integer","k":"token"}]}' + } +}