Skip to content

Commit b0d05a3

Browse files
authored
Prepare API generator for automatic check (#12175)
* Add ApiModificationTest * Fix docs * Add DocsGenerateOrderTest * No signature is generated for empty modules. Or for a module that contains only imports. * Move some methods to DumpTestUtils * Implement BindingSorter * BindingSorter takes care of extension methods * DocsGenerate respects order of bindings * DocsEmitSignatures emits correct markdown format * No signatures for synthetic modules * Conversion methods are sorted after instance methods * Ensure generated docs dir has same structure as src dir * Update Signatures_Spec * Use ScalaConversions
1 parent 2ca2880 commit b0d05a3

File tree

12 files changed

+981
-84
lines changed

12 files changed

+981
-84
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package org.enso.compiler.docs;
2+
3+
import java.util.HashSet;
4+
import java.util.List;
5+
import java.util.Set;
6+
import org.enso.compiler.core.ir.Module;
7+
import org.enso.compiler.core.ir.module.scope.Definition;
8+
import org.enso.compiler.core.ir.module.scope.Definition.Data;
9+
import org.enso.compiler.core.ir.module.scope.Definition.Type;
10+
import org.enso.compiler.core.ir.module.scope.definition.Method;
11+
import scala.jdk.javaapi.CollectionConverters;
12+
13+
/**
14+
* Bindings are sorted to categories. Every category is sorted alphabetically.
15+
* Categories are roughly:
16+
* <ul>
17+
* <li>Types</li>
18+
* <li>Instance and static methods on types</li>
19+
* <li>Module methods</li>
20+
* <li>Extension and conversion methods</li>
21+
* </ul>
22+
*/
23+
public final class BindingSorter {
24+
private BindingSorter() {}
25+
26+
/**
27+
* Returns sorted list of bindings defined on the given {@code moduleIr}.
28+
*/
29+
public static List<Definition> sortedBindings(Module moduleIr) {
30+
var bindings = CollectionConverters.asJava(moduleIr.bindings());
31+
var comparator = new BindingComparator(moduleIr);
32+
return bindings.stream().sorted(comparator).toList();
33+
}
34+
35+
public static List<Definition.Data> sortConstructors(List<Definition.Data> constructors) {
36+
var comparator = new ConstructorComparator();
37+
return constructors.stream().sorted(comparator).toList();
38+
}
39+
40+
41+
private static int compareTypes(Type type1, Type type2) {
42+
return type1.name().name().compareTo(type2.name().name());
43+
}
44+
45+
46+
private static final class BindingComparator implements java.util.Comparator<Definition> {
47+
private final Module moduleIr;
48+
private Set<String> typeNames;
49+
50+
private BindingComparator(Module moduleIr) {
51+
this.moduleIr = moduleIr;
52+
}
53+
54+
@Override
55+
public int compare(Definition def1, Definition def2) {
56+
return switch (def1) {
57+
case Method method1 when def2 instanceof Method methods ->
58+
compareMethods(method1, methods);
59+
case Type type1 when def2 instanceof Type type2 ->
60+
compareTypes(type1, type2);
61+
case Type type1 when def2 instanceof Method method2 -> compareTypeAndMethod(type1, method2);
62+
case Method method1 when def2 instanceof Type type2 ->
63+
-compareTypeAndMethod(type2, method1);
64+
default -> throw new AssertionError("unexpected type " + def1.getClass());
65+
};
66+
}
67+
68+
private int compareTypeAndMethod(Type type, Method method) {
69+
if (method.typeName().isDefined()) {
70+
if (isExtensionMethod(method)) {
71+
return -1;
72+
}
73+
var typeName = type.name().name();
74+
var methodTypeName = method.typeName().get().name();
75+
if (typeName.equals(methodTypeName)) {
76+
return -1;
77+
} else {
78+
return typeName.compareTo(methodTypeName);
79+
}
80+
}
81+
return -1;
82+
}
83+
84+
85+
private int compareMethods(Method method1, Method method2) {
86+
return switch (method1) {
87+
case
88+
Method.Explicit explicitMethod1 when method2 instanceof Method.Explicit explicitMethod2 -> {
89+
if (explicitMethod1.isPrivate() != explicitMethod2.isPrivate()) {
90+
if (explicitMethod1.isPrivate()) {
91+
yield 1;
92+
} else {
93+
yield -1;
94+
}
95+
}
96+
if (isExtensionMethod(explicitMethod1) != isExtensionMethod(explicitMethod2)) {
97+
if (isExtensionMethod(explicitMethod1)) {
98+
yield 1;
99+
} else {
100+
yield -1;
101+
}
102+
}
103+
var type1 = explicitMethod1.methodReference().typePointer();
104+
var type2 = explicitMethod2.methodReference().typePointer();
105+
if (type1.isDefined() && type2.isDefined()) {
106+
// Both methods are instance or static methods - compare by type name
107+
var typeName1 = type1.get().name();
108+
var typeName2 = type2.get().name();
109+
if (typeName1.equals(typeName2)) {
110+
// Methods are defined on the same type
111+
yield explicitMethod1.methodName().name()
112+
.compareTo(explicitMethod2.methodName().name());
113+
} else {
114+
yield type1.get().name().compareTo(type2.get().name());
115+
}
116+
} else if (type1.isDefined() && !type2.isDefined()) {
117+
// Instance or static methods on types have precedence over module methods
118+
yield -1;
119+
} else if (!type1.isDefined() && type2.isDefined()) {
120+
yield 1;
121+
}
122+
assert !type1.isDefined() && !type2.isDefined();
123+
yield explicitMethod1.methodName().name()
124+
.compareTo(explicitMethod2.methodName().name());
125+
}
126+
// Comparison of conversion methods is not supported.
127+
case Method.Conversion conversion1 when method2 instanceof Method.Conversion conversion2 ->
128+
0;
129+
case Method.Explicit explicit when method2 instanceof Method.Conversion -> -1;
130+
case Method.Conversion conversion when method2 instanceof Method.Explicit -> 1;
131+
default -> throw new AssertionError(
132+
"Unexpected type: method1=%s, method2=%s".formatted(method1.getClass(),
133+
method2.getClass()));
134+
};
135+
}
136+
137+
/**
138+
* An extension method is a method that is defined on a type that is defined outside the
139+
* current module.
140+
*/
141+
private boolean isExtensionMethod(Method method) {
142+
if (method.typeName().isDefined()) {
143+
var typeName = method.typeName().get().name();
144+
return !typeNamesInModule().contains(typeName);
145+
}
146+
return false;
147+
}
148+
149+
private Set<String> typeNamesInModule() {
150+
if (typeNames == null) {
151+
typeNames = new HashSet<>();
152+
moduleIr.bindings().foreach(binding -> {
153+
if (binding instanceof Definition.Type type) {
154+
typeNames.add(type.name().name());
155+
}
156+
return null;
157+
});
158+
}
159+
return typeNames;
160+
}
161+
}
162+
163+
private static final class ConstructorComparator implements java.util.Comparator<Definition.Data> {
164+
165+
@Override
166+
public int compare(Data cons1, Data cons2) {
167+
return cons1.name().name().compareTo(cons2.name().name());
168+
}
169+
}
170+
}

engine/runtime-compiler/src/main/java/org/enso/compiler/docs/DocsEmitSignatures.java

+13-3
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,21 @@ public boolean visitUnknown(IR ir, PrintWriter w) throws IOException {
2121

2222
@Override
2323
public boolean visitModule(QualifiedName name, Module module, PrintWriter w) throws IOException {
24-
w.println("## Enso Signatures 1.0");
25-
w.println("## module " + name);
26-
return true;
24+
if (isEmpty(module)) {
25+
return false;
26+
} else {
27+
w.println("## Enso Signatures 1.0");
28+
w.println("## module " + name);
29+
return true;
30+
}
2731
}
2832

2933
@Override
3034
public void visitMethod(Definition.Type t, Method.Explicit m, PrintWriter w) throws IOException {
3135
if (t != null) {
3236
w.append(" - ");
3337
} else {
38+
w.append("- ");
3439
if (m.typeName().isDefined()) {
3540
var fqn = DocsUtils.toFqnOrSimpleName(m.typeName().get());
3641
w.append(fqn + ".");
@@ -43,6 +48,7 @@ public void visitMethod(Definition.Type t, Method.Explicit m, PrintWriter w) thr
4348
public void visitConversion(Method.Conversion c, PrintWriter w) throws IOException {
4449
assert c.typeName().isDefined() : "Conversions need type name: " + c;
4550
var fqn = DocsUtils.toFqnOrSimpleName(c.typeName().get());
51+
w.append("- ");
4652
w.append(fqn + ".");
4753
w.append(DocsVisit.toSignature(c));
4854
w.append(" -> ").println(fqn);
@@ -64,4 +70,8 @@ public void visitConstructor(Definition.Type t, Definition.Data d, PrintWriter w
6470
throws IOException {
6571
w.println(" - " + DocsVisit.toSignature(d));
6672
}
73+
74+
private static boolean isEmpty(Module mod) {
75+
return mod.bindings().isEmpty() && mod.exports().isEmpty();
76+
}
6777
}

engine/runtime-compiler/src/main/java/org/enso/compiler/docs/DocsGenerate.java

+35-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package org.enso.compiler.docs;
22

3+
import static org.enso.scala.wrapper.ScalaConversions.asJava;
4+
import static org.enso.scala.wrapper.ScalaConversions.asScala;
5+
36
import java.io.IOException;
47
import java.io.PrintWriter;
58
import java.util.IdentityHashMap;
@@ -10,8 +13,6 @@
1013
import org.enso.compiler.core.ir.module.scope.definition.Method;
1114
import org.enso.filesystem.FileSystem;
1215
import org.enso.pkg.QualifiedName;
13-
import scala.collection.immutable.Seq;
14-
import scala.jdk.CollectionConverters;
1516

1617
/** Generator of documentation for an Enso project. */
1718
public final class DocsGenerate {
@@ -32,28 +33,53 @@ public static <File> File write(
3233
DocsVisit visitor, org.enso.pkg.Package<File> pkg, Iterable<CompilerContext.Module> modules)
3334
throws IOException {
3435
var fs = pkg.fileSystem();
35-
var docs = fs.getChild(pkg.root(), "docs");
36-
var api = fs.getChild(docs, "api");
37-
fs.createDirectories(api);
36+
var apiDir = defaultOutputDir(pkg);
37+
fs.createDirectories(apiDir);
3838

3939
for (var module : modules) {
40+
if (module.isSynthetic()) {
41+
continue;
42+
}
4043
var ir = module.getIr();
4144
assert ir != null : "need IR for " + module;
4245
if (ir.isPrivate()) {
4346
continue;
4447
}
4548
var moduleName = module.getName();
46-
var dir = createPkg(fs, api, moduleName);
49+
var dir = createDirs(fs, apiDir, stripNamespace(moduleName));
4750
var md = fs.getChild(dir, moduleName.item() + ".md");
4851
try (var mdWriter = fs.newBufferedWriter(md);
4952
var pw = new PrintWriter(mdWriter)) {
5053
visitModule(visitor, moduleName, ir, pw);
5154
}
5255
}
56+
return apiDir;
57+
}
58+
59+
public static <File> File defaultOutputDir(org.enso.pkg.Package<File> pkg) {
60+
var fs = pkg.fileSystem();
61+
var docs = fs.getChild(pkg.root(), "docs");
62+
var api = fs.getChild(docs, "api");
5363
return api;
5464
}
5565

56-
private static <File> File createPkg(FileSystem<File> fs, File root, QualifiedName pkg)
66+
/**
67+
* Strips namespace part from the given qualified {@code name}.
68+
*
69+
* @param name
70+
*/
71+
private static QualifiedName stripNamespace(QualifiedName name) {
72+
if (!name.isSimple()) {
73+
var path = name.pathAsJava();
74+
assert path.size() >= 2;
75+
var dropped = path.subList(2, path.size());
76+
return new QualifiedName(asScala(dropped), name.item());
77+
} else {
78+
return name;
79+
}
80+
}
81+
82+
private static <File> File createDirs(FileSystem<File> fs, File root, QualifiedName pkg)
5783
throws IOException {
5884
var dir = root;
5985
for (var item : pkg.pathAsJava()) {
@@ -68,7 +94,7 @@ public static void visitModule(
6894
var dispatch = DocsDispatch.create(visitor, w);
6995

7096
if (dispatch.dispatchModule(moduleName, ir)) {
71-
var moduleBindings = asJava(ir.bindings());
97+
var moduleBindings = BindingSorter.sortedBindings(ir);
7298
var alreadyDispatched = new IdentityHashMap<IR, IR>();
7399
for (var b : moduleBindings) {
74100
if (alreadyDispatched.containsKey(b)) {
@@ -77,7 +103,7 @@ public static void visitModule(
77103
switch (b) {
78104
case Definition.Type t -> {
79105
if (dispatch.dispatchType(t)) {
80-
for (var d : asJava(t.members())) {
106+
for (var d : BindingSorter.sortConstructors(asJava(t.members()))) {
81107
if (!d.isPrivate()) {
82108
dispatch.dispatchConstructor(t, d);
83109
}
@@ -111,8 +137,4 @@ public static void visitModule(
111137
}
112138
}
113139
}
114-
115-
private static <T> Iterable<T> asJava(Seq<T> seq) {
116-
return CollectionConverters.IterableHasAsJava(seq).asJava();
117-
}
118140
}

0 commit comments

Comments
 (0)