diff --git a/build.gradle b/build.gradle index b6a7211b062..ce87b3cc9de 100644 --- a/build.gradle +++ b/build.gradle @@ -84,6 +84,9 @@ subprojects { testImplementation(libs.logback.classic) + // Therapi runtime javadoc + annotationProcessor 'com.github.therapi:therapi-runtime-javadoc-scribe:0.13.0' + implementation 'com.github.therapi:therapi-runtime-javadoc:0.13.0' compileOnly(libs.jspecify) testCompileOnly(libs.jspecify) compileOnly(libs.checkerframework.qual) @@ -316,7 +319,6 @@ subprojects { afterEvaluate { description = project.description assert (project.description != null && project.description != "") : "Description of modules are required at maven-central" - print(project.description) } url = 'https://key-project.org/' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 136679b8cd5..7a1fd34ea47 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ stringtemplate = "4.3.4" javapoet = "1.13.0" javaparser = "3.28.0-K13.6" truth = "1.4.5" +therapi = "0.13.0" # UI and CLI flatlaf = "3.7.2" @@ -35,6 +36,7 @@ docking-frames = "1.1.3p1" # External tools scala-isabelle = "0.4.5" + [libraries] # SLF4J and logging slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } @@ -43,6 +45,10 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo # JSpecify (nullability annotations) jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" } +#Extracting Javadoc +therapi-processor = { module = "com.github.therapi:therapi-runtime-javadoc-scribe", version.ref = "therapi" } +therapi-runtime = { module = "com.github.therapi:therapi-runtime-javadoc", version.ref = "therapi" } + # EISOP Checker Framework checkerframework-qual = { module = "io.github.eisop:checker-qual", version.ref = "eisop" } checkerframework-util = { module = "io.github.eisop:checker-util", version.ref = "eisop" } diff --git a/key.core/build.gradle b/key.core/build.gradle index c6584ca35ab..7567393c7fb 100644 --- a/key.core/build.gradle +++ b/key.core/build.gradle @@ -24,6 +24,9 @@ dependencies { testImplementation(libs.truth) testImplementation(project(":key.core")) + annotationProcessor(libs.therapi.processor) + api(libs.therapi.runtime) + // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml testImplementation(libs.jackson.dataformat.yaml) @@ -32,7 +35,7 @@ dependencies { } tasks.withType(JavaCompile).configureEach { - options.compilerArgs << "-Ajavadoc.packages=de.uka.ilkd.key.scripts" + options.compilerArgs << "-Ajavadoc.packages=de.uka.ilkd.key.scripts,de.uka.ilkd.key.rule.conditions" } // The target directory for JavaCC (parser generation) diff --git a/key.core/src/main/java/de/uka/ilkd/key/doc/ScriptDoc.java b/key.core/src/main/java/de/uka/ilkd/key/doc/ScriptDoc.java new file mode 100644 index 00000000000..ae9d19210a5 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/doc/ScriptDoc.java @@ -0,0 +1,27 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.doc; + +import java.util.ArrayList; +import java.util.Comparator; + +import de.uka.ilkd.key.scripts.ProofScriptCommand; +import de.uka.ilkd.key.scripts.ProofScriptEngine; + +/** + * + * @author Alexander Weigl + * @version 1 (23.08.26) + */ +public class ScriptDoc { + public static void main(String[] args) { + var commands = new ArrayList<>(ProofScriptEngine.loadCommands().values()); + commands.sort(Comparator.comparing(ProofScriptCommand::getName)); + + for (var command : commands) { + System.out.println(command.getDocumentation()); + System.out.println("\n\n\n"); + } + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/doc/VarcondDoc.java b/key.core/src/main/java/de/uka/ilkd/key/doc/VarcondDoc.java new file mode 100644 index 00000000000..08403ae95a2 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/doc/VarcondDoc.java @@ -0,0 +1,107 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.doc; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.function.Function; +import java.util.stream.Collectors; + +import de.uka.ilkd.key.nparser.varexp.TacletBuilderCommand; +import de.uka.ilkd.key.nparser.varexp.TacletBuilderCommandInfo; +import de.uka.ilkd.key.nparser.varexp.TacletBuilderManipulators; + +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.StaticJavaParser; +import com.github.therapi.runtimejavadoc.CommentFormatter; +import com.google.common.collect.Streams; +import org.jspecify.annotations.Nullable; + +/** + * + * @author Alexander Weigl + * @version 1 (23.08.26) + */ +public class VarcondDoc { + // region + public static void main(String[] args) { + var config = new ParserConfiguration(); + config.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); + StaticJavaParser.setConfiguration(config); + + Function normalizeCmdName = + (String it) -> it.startsWith("\\") ? it.replace("\\\\", "\\") : "\\" + it; + Function getTriggerName = TacletBuilderCommandInfo::name; + + var g = TacletBuilderManipulators.getConditionBuilders().stream() + .map(TacletBuilderCommand::getInformation) + .collect(Collectors.groupingBy(getTriggerName.andThen(normalizeCmdName))); + Comparator reversed = + Comparator.comparing((TacletBuilderCommandInfo it) -> it.argumentTypes().length) + .reversed(); + + var conds = g.keySet().stream().sorted().toList(); + + for (var name : conds) { + var cmds = g.get(name); + System.out.println(); + System.out.println(); + System.out.format("### `%s`\n\n", name); + cmds.sort(reversed); + + final var generalDocumentation = cmds.getFirst().getGeneralDocumentation(); + System.out.println( + cleanJavadoc(new CommentFormatter().format(generalDocumentation.getComment()))); + + System.out.format("\n**Signatures**\n\n"); + for (TacletBuilderCommandInfo cmd : cmds) { + + final var arguments = Streams.zip( + Arrays.stream(cmd.argNames()), + Arrays.stream(cmd.argumentTypes()).map(Enum::toString), + "%s: %s"::formatted) + .collect(Collectors.joining(", ")); + System.out.printf("* `%s(%s)`\n", name, arguments); + + if (cmd.isNegationSupported()) { + System.out.printf("* `\\not%s(%s)`\n", name, arguments); + } + + System.out.println(); + final var argumentInformation = cmd.getArgumentInformation(); + + System.out.println( + indent(" ", cleanJavadoc(argumentInformation.getComment().toString()))); + System.out.println(); + argumentInformation.getParams().stream() + .map(tag -> " * `%s` %s".formatted(tag.getName(), tag.getComment())) + .forEach(System.out::println); + System.out.println(); + } + } + } + + private static String indent(String spaces, String it) { + return spaces + it.replace("\n", "\n" + spaces); + } + + private static String cleanJavadoc(@Nullable String it) { + if (it == null) + it = ""; + return it.replace("", "`") + .replace("
    ", "\n") + .replace("
", "\n") + .replace("
    ", "\n") + .replace("
  • ", "* ") + .replace("
  • ", "") + .replace("{@link", "`") + .replace("}", "`") + .replace("", "`") + .replace("
    ", "`") + .replace("", "`") + .replace("", "**") + .replace("", "**"); + } + // endregion +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/builder/TacletPBuilder.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/builder/TacletPBuilder.java index dd4db64b434..e5fa25cbec6 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/builder/TacletPBuilder.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/builder/TacletPBuilder.java @@ -800,7 +800,7 @@ public Object visitVarexp(JavaKeYParser.VarexpContext ctx) { } if (!applied) { LOGGER.warn("Found name-matching conditions with following type signature:"); - suitableManipulators.forEach(it -> LOGGER.warn(Arrays.toString(it.getArgumentTypes()))); + suitableManipulators.forEach(it -> LOGGER.warn(it.getArgumentTypes().toString())); LOGGER.warn("But you gave {} arguments.\n", arguments.size()); semanticError(ctx, "Could not apply the given variable condition: %s", ctx.getText()); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/AbstractConditionBuilder.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/AbstractConditionBuilder.java index b5bbef4ac18..44d95a67b3b 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/AbstractConditionBuilder.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/AbstractConditionBuilder.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-only */ package de.uka.ilkd.key.nparser.varexp; -import org.jspecify.annotations.NonNull; +import org.key_project.prover.rules.VariableCondition; /** * @author Alexander Weigl @@ -11,8 +11,12 @@ */ public abstract class AbstractConditionBuilder extends AbstractTacletBuilderCommand implements ConditionBuilder { - protected AbstractConditionBuilder(@NonNull String triggerName, - @NonNull ArgumentType... argumentsTypes) { - super(triggerName, argumentsTypes); + protected AbstractConditionBuilder(TacletBuilderCommandInfo info) { + super(info); + } + + public AbstractConditionBuilder(String name, Class clazz, + boolean negationSupported, ArgumentType... types) { + super(TacletBuilderCommandInfo.createVarcondInfo(name, clazz, negationSupported, types)); } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/AbstractTacletBuilderCommand.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/AbstractTacletBuilderCommand.java index ca97d45ba2f..68110b6d7aa 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/AbstractTacletBuilderCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/AbstractTacletBuilderCommand.java @@ -3,7 +3,6 @@ * SPDX-License-Identifier: GPL-2.0-only */ package de.uka.ilkd.key.nparser.varexp; -import org.jspecify.annotations.NonNull; /** * Simple default implementation for {@link TacletBuilderCommand}. @@ -12,36 +11,26 @@ * @version 1 (12/9/19) */ public abstract class AbstractTacletBuilderCommand implements TacletBuilderCommand { - private final @NonNull String triggerName; - private final @NonNull ArgumentType[] argumentsTypes; + protected final TacletBuilderCommandInfo info; - /** - * Construct this class with the parameters for {@link #isSuitableFor(String)} and - * {@link #getArgumentTypes()}. - * - * @param triggerName the name of this command. - * @param argumentsTypes the argument type of this command. - */ - protected AbstractTacletBuilderCommand(@NonNull String triggerName, - @NonNull ArgumentType... argumentsTypes) { - this.triggerName = triggerName; - this.argumentsTypes = argumentsTypes; + protected AbstractTacletBuilderCommand(TacletBuilderCommandInfo info) { + this.info = info; } @Override - public boolean isSuitableFor(@NonNull String name) { - if (triggerName.equalsIgnoreCase(name)) { + public boolean isSuitableFor(String name) { + if (info.name().equalsIgnoreCase(name)) { return true; } - if (name.startsWith("\\")) // handling leading backslashes - { + // handling leading backslashes + if (name.startsWith("\\")) { return isSuitableFor(name.substring(1)); } return false; } @Override - public ArgumentType[] getArgumentTypes() { - return argumentsTypes; + public TacletBuilderCommandInfo getInformation() { + return info; } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/ArgumentType.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/ArgumentType.java index 72180ecad0a..da1b2166041 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/ArgumentType.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/ArgumentType.java @@ -12,14 +12,19 @@ /** * Argument types for {@link TacletBuilderCommand}s. + * Each {@link ArgumentType} has an expected class for the type of the argument. * * @author Alexander Weigl * @version 1 (12/9/19) * @see TacletBuilderCommand */ public enum ArgumentType { - TYPE_RESOLVER(TypeResolver.class), SORT(Sort.class), TERM(JTerm.class), - JAVA_TYPE(KeYJavaType.class), VARIABLE(ParsableVariable.class), STRING(String.class); + TYPE_RESOLVER(TypeResolver.class), + SORT(Sort.class), + TERM(JTerm.class), + JAVA_TYPE(KeYJavaType.class), + VARIABLE(ParsableVariable.class), + STRING(String.class); public final Class clazz; diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/ConstructorBasedBuilder.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/ConstructorBasedBuilder.java index a5353a2ef01..c481998223f 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/ConstructorBasedBuilder.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/ConstructorBasedBuilder.java @@ -12,39 +12,26 @@ public class ConstructorBasedBuilder extends AbstractConditionBuilder { private final Class clazz; - private final boolean negationSupported; public ConstructorBasedBuilder(String name, Class clazz, ArgumentType... types) { - this(name, lastArgumentOfFirstContructorIsBoolean(clazz), clazz, types); + this(TacletBuilderCommandInfo.createVarcondInfo(name, clazz, types), clazz); } - private static boolean lastArgumentOfFirstContructorIsBoolean( + public ConstructorBasedBuilder(TacletBuilderCommandInfo info, Class clazz) { - try { - Class[] types = clazz.getConstructors()[0].getParameterTypes(); - return types[types.length - 1] == Boolean.class - || types[types.length - 1] == Boolean.TYPE; - } catch (ArrayIndexOutOfBoundsException e) { - return false; - } - } - - public ConstructorBasedBuilder(String name, boolean negationSupported, - Class clazz, ArgumentType... types) { - super(name, types); + super(info); this.clazz = clazz; - this.negationSupported = negationSupported; } @Override public VariableCondition build(Object[] arguments, List parameters, boolean negated) { - if (negated && !negationSupported) { + if (negated && !info.isNegationSupported()) { throw new RuntimeException(clazz.getName() + " does not support negation."); } Object[] args = arguments; - if (negationSupported) { + if (info.isNegationSupported()) { args = Arrays.copyOf(arguments, arguments.length + 1); args[args.length - 1] = negated; } @@ -58,4 +45,9 @@ public VariableCondition build(Object[] arguments, List parameters, bool } throw new RuntimeException(); } + + @Override + public TacletBuilderCommandInfoImpl getInformation() { + return (TacletBuilderCommandInfoImpl) super.getInformation(); + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderCommand.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderCommand.java index 60e1ba39ab2..c2687e75881 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderCommand.java @@ -27,6 +27,9 @@ public interface TacletBuilderCommand { */ boolean isSuitableFor(@NonNull String name); + /// Returns information about this command. + TacletBuilderCommandInfo getInformation(); + /** * Defines the amount and type of expected arguments. For example, if you want describe a * sub-type test (instanceOf) you would need two sorts {@code new ArgumentType[]{SORT,SORT} } as @@ -38,7 +41,9 @@ public interface TacletBuilderCommand { * * @see ArgumentType */ - ArgumentType[] getArgumentTypes(); + default ArgumentType[] getArgumentTypes() { + return getInformation().argumentTypes(); + } /** * Applying this command on the given taclet builder. diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderCommandInfo.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderCommandInfo.java new file mode 100644 index 00000000000..225fb5a1247 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderCommandInfo.java @@ -0,0 +1,291 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.nparser.varexp; + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.List; + +import com.github.therapi.runtimejavadoc.ClassJavadoc; +import com.github.therapi.runtimejavadoc.MethodJavadoc; +import com.github.therapi.runtimejavadoc.ParamJavadoc; +import com.github.therapi.runtimejavadoc.RuntimeJavadoc; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/// Describes a single "variable condition" (varcond) or taclet-builder command that can be +/// used inside taclet definitions, together with metadata needed to parse, validate, and +/// document its usage. +/// +/// An instance of this interface bundles together: +/// +/// - the command's [`name`][#name()] as it appears in taclet source files, +/// - the expected [`argument types`][#argumentTypes()], +/// - whether the command [`supports negation`][#isNegationSupported()] (i.e. an +/// additional trailing `boolean` constructor argument), +/// - and Javadoc-derived documentation for both the command itself and its individual +/// arguments, extracted via reflection from the backing implementation class. +/// +/// Instances are typically created via the [#createVarcondInfo] factory methods rather +/// than by implementing this interface directly. +/// +/// @author Alexander Weigl +/// @version 1 (23.08.26) +@NullMarked +public interface TacletBuilderCommandInfo { + + /// Returns the name of this command as used in taclet source files. + /// + /// @return the command name, never `null` + String name(); + + /// Returns the declared types of the arguments accepted by this command, in the order + /// they must appear in the taclet source. + /// + /// @return the array of argument types + ArgumentType[] argumentTypes(); + + /// Returns the names of the arguments as declared in the backing implementation's + /// constructor (or as documented via Javadoc, if available). + /// + /// Resolving the argument names requires reflecting on the implementation class and is + /// performed lazily on first access. + /// + /// @return the array of argument names, in the same order as [#argumentTypes()] + String[] argNames(); + + /// Indicates whether this command supports an optional trailing negation flag, i.e. + /// whether the backing implementation class has a constructor whose parameters are the + /// declared [`argument types`][#argumentTypes()] followed by an additional + /// `boolean` parameter. + /// + /// @return `true` if negation is supported, `false` otherwise + boolean isNegationSupported(); + + /// Returns the class-level Javadoc documentation extracted from the backing + /// implementation class. This is typically used as the general description of what the + /// command does. + /// + /// @return the class-level Javadoc, never `null` + ClassJavadoc getGeneralDocumentation(); + + /// Returns the Javadoc documentation of the specific constructor that matches this + /// command's argument types (and negation flag, if supported). This is typically used + /// to document the individual arguments of the command. + /// + /// @return the matching constructor's Javadoc, or a documentation object with empty + /// fields if no Javadoc could be resolved + MethodJavadoc getArgumentInformation(); + + /// Creates a [TacletBuilderCommandInfo] for a command whose negation support is + /// determined automatically by inspecting the constructors of `clazz`. + /// + /// @param name the name of the command as used in taclet source files + /// @param clazz the implementation class backing this command + /// @param types the expected argument types, in declaration order + /// @return a new [TacletBuilderCommandInfo] instance + static TacletBuilderCommandInfo createVarcondInfo(String name, Class clazz, + ArgumentType... types) { + return new TacletBuilderCommandInfoImpl(name, types, clazz, null); + } + + /// Creates a [TacletBuilderCommandInfo] for a command with an explicitly specified + /// negation support flag, instead of determining it via reflection. + /// + /// @param name the name of the command as used in taclet source files + /// @param clazz the implementation class backing this command + /// @param negationSupported whether the command supports a trailing negation argument + /// @param types the expected argument types, in declaration order + /// @return a new [TacletBuilderCommandInfo] instance + static TacletBuilderCommandInfo createVarcondInfo(String name, Class clazz, + Boolean negationSupported, ArgumentType... types) { + return new TacletBuilderCommandInfoImpl(name, types, clazz, negationSupported); + } +} + + +/// Default implementation of [TacletBuilderCommandInfo]. +/// +/// Argument names and documentation are resolved lazily, on first access, by reflecting on +/// the backing implementation class ([#clazz]) and looking up its runtime Javadoc via +/// [RuntimeJavadoc]. +class TacletBuilderCommandInfoImpl implements TacletBuilderCommandInfo { + /// The name of the command as used in taclet source files. + public final String name; + /// The expected argument types, in declaration order. + private final ArgumentType[] argTypes; + /// The implementation class backing this command, used for reflection-based lookups. + private final Class clazz; + /// Whether this command supports a trailing negation argument. `null` until + /// lazily resolved via [#isNegationSupported()]. + private @Nullable Boolean isNegationSupported; + /// The resolved argument names, or `null` until [#findDocumentation()] has run. + private String @Nullable [] argNames; + /// The resolved class-level Javadoc, or `null` until [#findDocumentation()] has run. + private @Nullable ClassJavadoc generalDocumentation; + /// The resolved constructor Javadoc, or `null` until [#findDocumentation()] has run. + private @Nullable MethodJavadoc argumentDocumentation; + + /// Creates a new command descriptor. + /// + /// @param name the name of the command as used in taclet source files + /// @param types the expected argument types, in declaration order + /// @param clazz the implementation class backing this command + /// @param negationSupported whether the command supports a trailing negation argument, + /// or `null` to determine this lazily via reflection + public TacletBuilderCommandInfoImpl(String name, ArgumentType[] types, Class clazz, + @Nullable Boolean negationSupported) { + this.name = name; + argTypes = types; + this.clazz = clazz; + isNegationSupported = negationSupported; + } + + @Override + public String name() { + return name; + } + + @Override + public ArgumentType[] argumentTypes() { + return argTypes; + } + + @Override + public String[] argNames() { + if (argNames == null) { + findDocumentation(); + } + return argNames; + } + + @Override + public boolean isNegationSupported() { + if (isNegationSupported == null) { + isNegationSupported = lastArgumentOfFirstConstructorIsBoolean(clazz, argTypes); + } + return isNegationSupported; + } + + @Override + public ClassJavadoc getGeneralDocumentation() { + if (generalDocumentation == null) { + findDocumentation(); + } + return generalDocumentation; + } + + @Override + public MethodJavadoc getArgumentInformation() { + if (argumentDocumentation == null) { + findDocumentation(); + } + return argumentDocumentation; + } + + /// Computes the parameter types of the constructor that this command's arguments (and, + /// if applicable, its negation flag) would map to. + /// + /// @return the array of expected constructor parameter types + Class[] getConstructorClasses() { + return getConstructorClasses(argTypes, isNegationSupported()); + } + + /// Resolves and caches [#argNames], [#generalDocumentation], and + /// [#argumentDocumentation] by reflecting on [#clazz] and looking up its + /// matching constructor's runtime Javadoc. + /// + /// If no constructor matching [#getConstructorClasses()] can be found, argument + /// names are filled with empty strings and empty documentation placeholders are used + /// instead of failing. + private void findDocumentation() { + ClassJavadoc classDoc = RuntimeJavadoc.getJavadoc(clazz.getName()); + generalDocumentation = classDoc; + + final var constr = findConstructor(clazz, getConstructorClasses()); + if (constr == null) { + argNames = new String[argTypes.length]; + Arrays.fill(argNames, ""); + argumentDocumentation = + MethodJavadoc.createEmpty((Constructor) null); + return; + } + + final var constructorDeclaration = classDoc.getConstructors() + .stream().filter(it -> it.matches(constr)).findAny(); + + final var parameters = constr.getParameters(); + argNames = new String[argTypes.length]; + for (int i = 0; i < argTypes.length; i++) { + argNames[i] = parameters[i].getName(); + } + + constructorDeclaration.ifPresent(it -> { + List params = it.getParams(); + for (int i = 0; i < argNames.length; i++) { + argNames[i] = params.get(i).getName(); + } + }); + + argumentDocumentation = constructorDeclaration + .orElse(MethodJavadoc.createEmpty((Constructor) null)); + // endregion + } + + + /// Checks whether `clazz` declares a constructor whose parameter types are + /// `argTypes` followed by a trailing `boolean` parameter, which indicates + /// support for an optional negation flag. + /// + /// @param clazz the implementation class to inspect + /// @param argTypes the expected leading argument types + /// @return `true` if such a constructor exists, `false` otherwise + /// @throws IllegalStateException if no suitable constructor exists in `clazz` + private static boolean lastArgumentOfFirstConstructorIsBoolean( + Class clazz, ArgumentType[] argTypes) { + if (findConstructor(clazz, getConstructorClasses(argTypes, true)) != null) { + return true; + } + if (findConstructor(clazz, getConstructorClasses(argTypes, false)) != null) { + return false; + } + throw new IllegalStateException(); + } + + private static @Nullable Constructor findConstructor(Class clazz, + Class[] constructorClasses) { + final var constructors = clazz.getConstructors(); + c: for (var constructor : constructors) { + if (constructor.getParameterCount() != constructorClasses.length) + continue; + final var parameterTypes = constructor.getParameterTypes(); + for (var i = 0; i < parameterTypes.length; i++) { + if (!constructorClasses[i].isAssignableFrom(parameterTypes[i])) { + continue c; + } + } + return constructor; + } + return null; + } + + /// Builds the array of constructor parameter types corresponding to `argTypes`, + /// optionally appending `boolean.class` for negation support. + /// + /// @param argTypes the declared argument types + /// @param isNegationSupported whether a trailing `boolean` parameter should be + /// appended for negation support + /// @return the resulting array of constructor parameter types + static Class[] getConstructorClasses(ArgumentType[] argTypes, boolean isNegationSupported) { + Class[] clazzes = new Class[argTypes.length + (isNegationSupported ? 1 : 0)]; + for (int i = 0; i < argTypes.length; i++) { + clazzes[i] = argTypes[i].clazz; + } + + if (isNegationSupported) + clazzes[clazzes.length - 1] = Boolean.TYPE; + return clazzes; + } + +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderManipulators.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderManipulators.java index 67065ce497c..4a0df255104 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderManipulators.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/TacletBuilderManipulators.java @@ -14,17 +14,19 @@ import de.uka.ilkd.key.logic.op.JOperatorSV; import de.uka.ilkd.key.logic.op.ProgramSV; import de.uka.ilkd.key.logic.sort.GenericSort; +import de.uka.ilkd.key.rule.NewVarcond; import de.uka.ilkd.key.rule.conditions.*; import de.uka.ilkd.key.rule.tacletbuilder.TacletBuilder; import org.key_project.logic.op.sv.SchemaVariable; import org.key_project.logic.sort.Sort; import org.key_project.prover.rules.VariableCondition; - -import org.jspecify.annotations.NonNull; +import org.key_project.prover.rules.conditions.NewDependingOn; +import org.key_project.prover.rules.conditions.NotFreeIn; import static de.uka.ilkd.key.nparser.varexp.ArgumentType.SORT; import static de.uka.ilkd.key.nparser.varexp.ArgumentType.TYPE_RESOLVER; +import static de.uka.ilkd.key.nparser.varexp.TacletBuilderCommandInfo.createVarcondInfo; import static de.uka.ilkd.key.rule.conditions.TypeComparisonCondition.Mode.*; /** @@ -36,6 +38,7 @@ */ public class TacletBuilderManipulators { // region Factories + // Short cut for argument types private static final ArgumentType TR = TYPE_RESOLVER; private static final ArgumentType KJT = ArgumentType.JAVA_TYPE; @@ -50,20 +53,14 @@ public class TacletBuilderManipulators { private static final ArgumentType T = ArgumentType.TERM; - /** - * - */ public static final AbstractConditionBuilder ABSTRACT_OR_INTERFACE = new ConstructorBasedBuilder("isAbstractOrInterface", AbstractOrInterfaceType.class, TR); public static final AbstractConditionBuilder FINAL_TYPE = new ConstructorBasedBuilder("isFinal", FinalTypeVarCond.class, TR); - /** - * - */ public static final AbstractConditionBuilder SAME = - new AbstractConditionBuilder("same", TR, TR) { + new AbstractConditionBuilder("same", TypeComparisonCondition.class, true, TR, TR) { @Override public TypeComparisonCondition build(Object[] arguments, List parameters, boolean negated) { @@ -73,11 +70,8 @@ public TypeComparisonCondition build(Object[] arguments, List parameters } }; - /** - * - */ public static final AbstractConditionBuilder IS_SUBTYPE = - new AbstractConditionBuilder("sub", TR, TR) { + new AbstractConditionBuilder("sub", TypeComparisonCondition.class, true, TR, TR) { @Override public TypeComparisonCondition build(Object[] arguments, List parameters, boolean negated) { @@ -91,9 +85,9 @@ public TypeComparisonCondition build(Object[] arguments, List parameters * */ public static final AbstractConditionBuilder STRICT = - new AbstractConditionBuilder("scrictSub", TR, TR) { + new AbstractConditionBuilder("scrictSub", TypeComparisonCondition.class, false, TR, TR) { @Override - public boolean isSuitableFor(@NonNull String name) { + public boolean isSuitableFor(String name) { if (super.isSuitableFor(name)) { return true; } @@ -116,7 +110,8 @@ public TypeComparisonCondition build(Object[] arguments, List parameters * */ public static final AbstractConditionBuilder DISJOINT_MODULO_NULL = - new AbstractConditionBuilder("disjointModuloNull", TR, TR) { + new AbstractConditionBuilder("disjointModuloNull", TypeComparisonCondition.class, false, TR, + TR) { @Override public TypeComparisonCondition build(Object[] arguments, List parameters, boolean negated) { @@ -138,7 +133,8 @@ public TypeComparisonCondition build(Object[] arguments, List parameters * */ public static final AbstractTacletBuilderCommand NEW_JAVATYPE = - new AbstractTacletBuilderCommand("new", SV, KJT) { + new AbstractTacletBuilderCommand( + createVarcondInfo("new", NewVarcond.class, false, SV, KJT)) { @Override public void apply(TacletBuilder tacletBuilder, Object[] arguments, List parameters, boolean negated) { @@ -151,7 +147,8 @@ public void apply(TacletBuilder tacletBuilder, Object[] arguments, }; public static final AbstractTacletBuilderCommand NEW_VAR = - new AbstractTacletBuilderCommand("new", SV, SORT) { + new AbstractTacletBuilderCommand( + createVarcondInfo("new", NewVarcond.class, false, SV, SORT)) { @Override public void apply(TacletBuilder tacletBuilder, Object[] arguments, List parameters, boolean negated) { @@ -168,8 +165,8 @@ public void apply(TacletBuilder tacletBuilder, Object[] arguments, "newLocalVars", NewLocalVarsCondition.class, SV, SV, SV, SV); static class NotFreeInTacletBuilderCommand extends AbstractTacletBuilderCommand { - public NotFreeInTacletBuilderCommand(@NonNull ArgumentType... argumentsTypes) { - super("notFreeIn", argumentsTypes); + public NotFreeInTacletBuilderCommand(ArgumentType... argumentsTypes) { + super(createVarcondInfo("notFreeIn", NotFreeIn.class, true, argumentsTypes)); } @Override @@ -195,7 +192,8 @@ public void apply(TacletBuilder tacletBuilder, Object[] arguments, private static final List tacletBuilderCommands = new ArrayList<>(32); public static final AbstractTacletBuilderCommand NEW_TYPE_OF = - new AbstractTacletBuilderCommand("newTypeOf", SV, SV) { + new AbstractTacletBuilderCommand( + createVarcondInfo("newTypeOf", NewVarcond.class, false, SV, SV)) { @Override public void apply(TacletBuilder tacletBuilder, Object[] arguments, @@ -209,7 +207,8 @@ public void apply(TacletBuilder tacletBuilder, Object[] arguments, } }; public static final AbstractTacletBuilderCommand NEW_DEPENDING_ON = - new AbstractTacletBuilderCommand("newDependingOn", SV, SV) { + new AbstractTacletBuilderCommand( + createVarcondInfo("newDependingOn", NewDependingOn.class, false, SV, SV)) { @Override public void apply(TacletBuilder tb, Object[] arguments, List parameters, boolean negated) { @@ -236,7 +235,8 @@ public void apply(TacletBuilder tb, Object[] arguments, List paramete public static final AbstractConditionBuilder ARRAY = new ConstructorBasedBuilder("isArray", ArrayTypeCondition.class, SV); public static final AbstractConditionBuilder REFERENCE_ARRAY = - new AbstractConditionBuilder("isReferenceArray", SV) { + new AbstractConditionBuilder("isReferenceArray", ArrayComponentTypeCondition.class, true, + SV) { @Override public VariableCondition build(Object[] arguments, List parameters, boolean negated) { @@ -252,7 +252,7 @@ public VariableCondition build(Object[] arguments, List parameters, public static final AbstractConditionBuilder THIS_REFERENCE = new ConstructorBasedBuilder("isThisReference", IsThisReference.class, SV); public static final AbstractConditionBuilder REFERENCE = - new AbstractConditionBuilder("isReference", TR) { + new AbstractConditionBuilder("isReference", TypeCondition.class, true, TR) { @Override public VariableCondition build(Object[] arguments, List parameters, boolean negated) { @@ -302,8 +302,8 @@ public VariableCondition build(Object[] arguments, List parameters, static class JavaTypeToSortConditionBuilder extends AbstractConditionBuilder { private final boolean elmen; - public JavaTypeToSortConditionBuilder(@NonNull String triggerName, boolean forceElmentary) { - super(triggerName, SV, SORT); + public JavaTypeToSortConditionBuilder(String triggerName, boolean forceElmentary) { + super(triggerName, JavaTypeToSortCondition.class, false, SV, SORT); this.elmen = forceElmentary; } @@ -332,7 +332,7 @@ public VariableCondition build(Object[] arguments, List parameters, new ConstructorBasedBuilder("hasLabel", TermLabelCondition.class, TSV, S); // endregion public static final AbstractConditionBuilder STORE_TERM_IN = - new AbstractConditionBuilder("storeTermIn", SV, T) { + new AbstractConditionBuilder("storeTermIn", StoreTermInCondition.class, false, SV, T) { @Override public VariableCondition build(Object[] arguments, List parameters, boolean negated) { @@ -350,7 +350,7 @@ public VariableCondition build(Object[] arguments, List parameters, public static final AbstractConditionBuilder GET_FREE_INVARIANT = new ConstructorBasedBuilder( "\\getFreeInvariant", LoopFreeInvariantCondition.class, PV, SV, SV); public static final AbstractConditionBuilder GET_VARIANT = - new AbstractConditionBuilder("\\getVariant", PV, SV) { + new AbstractConditionBuilder("\\getVariant", LoopVariantCondition.class, false, PV, SV) { @Override public VariableCondition build(Object[] arguments, List parameters, boolean negated) { @@ -359,7 +359,7 @@ public VariableCondition build(Object[] arguments, List parameters, } }; public static final AbstractConditionBuilder IS_LABELED = - new AbstractConditionBuilder("isLabeled", PV) { + new AbstractConditionBuilder("isLabeled", IsLabeledCondition.class, true, PV) { @Override public IsLabeledCondition build(Object[] arguments, List parameters, boolean negated) { diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/package-info.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/package-info.java new file mode 100644 index 00000000000..de15cc91730 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/varexp/package-info.java @@ -0,0 +1,9 @@ +/** + * + * @author Alexander Weigl + * @version 1 (23.08.26) + */ +@NullMarked +package de.uka.ilkd.key.nparser.varexp; + +import org.jspecify.annotations.NullMarked; diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ArgumentsLifter.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ArgumentsLifter.java index 77cf9145bcc..685a64a041e 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ArgumentsLifter.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ArgumentsLifter.java @@ -5,10 +5,16 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; import de.uka.ilkd.key.scripts.ProofScriptCommand; +import com.github.therapi.runtimejavadoc.ClassJavadoc; +import com.github.therapi.runtimejavadoc.FieldJavadoc; +import com.github.therapi.runtimejavadoc.RuntimeJavadoc; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -80,7 +86,7 @@ public static String generateCommandUsage(String commandName, Class parameter } public static String extractDocumentation(String command, Class commandClazz, - Class parameterClazz) { + @Nullable Class parameterClazz) { StringBuilder sb = new StringBuilder(); Deprecated dep = commandClazz.getAnnotation(Deprecated.class); @@ -89,22 +95,33 @@ public static String extractDocumentation(String command, Class commandClazz, "**Caution! This proof script command is deprecated, and may be removed soon!**\n\n"); } + ClassJavadoc jdocCommand = RuntimeJavadoc.getJavadoc(commandClazz); + Documentation docCommand = commandClazz.getAnnotation(Documentation.class); if (docCommand != null) { sb.append(docCommand.value()); sb.append("\n\n"); + } else { + sb.append(jdocCommand.getComment()); + sb.append("\n\n"); } if (parameterClazz == null) { return sb.toString(); } + ClassJavadoc jdocParams = RuntimeJavadoc.getJavadoc(parameterClazz); + Documentation docAn = parameterClazz.getAnnotation(Documentation.class); if (docAn != null) { sb.append(docAn.value()); sb.append("\n\n"); + } else { + sb.append(jdocParams.getComment()); + sb.append("\n\n"); } + sb.append("#### Usage: \n`").append(generateCommandUsage(command, parameterClazz)) .append("`\n\n"); @@ -113,13 +130,21 @@ public static String extractDocumentation(String command, Class commandClazz, sb.append("#### Parameters:\n"); for (ProofScriptArgument meta : args) { sb.append("\n\n"); + + var documentation = meta.getDocumentation(); + if (documentation.isEmpty()) { + documentation = jdocParams.getFields().stream() + .filter(it -> it.getName().equals(meta.getField().getName())) + .findFirst().map(FieldJavadoc::toString).orElse(""); + } + if (meta.isPositional()) { sb.append("* `%s` *(%s%s positional argument, type %s)*:
    %s".formatted( meta.getName(), meta.isRequired() ? "" : "optional ", ordinalStr(meta.getArgumentPosition() + 1), meta.getField().getType().getSimpleName(), - meta.getDocumentation())); + documentation)); } if (meta.isOption()) { @@ -127,28 +152,28 @@ public static String extractDocumentation(String command, Class commandClazz, meta.getName(), meta.isRequired() ? "" : "optional ", meta.getField().getType().getSimpleName(), - meta.getDocumentation())); + documentation)); } if (meta.isFlag()) { sb.append("* `%s` *(flag)*:
    %s".formatted( meta.getName(), - meta.getDocumentation())); + documentation)); } if (meta.isPositionalVarArgs()) { - sb.append("* `%s...` (%s): %s".formatted( + sb.append("* `%s...` (%s): %s
    %s".formatted( meta.getName(), meta.getPositionalVarargs().as(), meta.getPositionalVarargs().startIndex(), - meta.getDocumentation())); + documentation)); } if (meta.isOptionalVarArgs()) { sb.append("* `%s...`: *(options prefixed by `%s`, type %s)*:
    %s".formatted( meta.getName(), meta.getOptionalVarArgs().prefix(), meta.getOptionalVarArgs().as().getSimpleName(), - meta.getDocumentation())); + documentation)); } } @@ -188,39 +213,6 @@ public static String extractCategory(Class command private static @NonNull List getSortedProofScriptArguments( Class parameterClazz) { - // Comparator optional = - // Comparator.comparing(ProofScriptArgument::isOption); - // Comparator positional = - // Comparator.comparing(ProofScriptArgument::isPositional); - // Comparator flagal = - // Comparator.comparing(ProofScriptArgument::isFlag); - // Comparator allargsal = - // Comparator.comparing(ProofScriptArgument::isPositionalVarArgs); - // Comparator byRequired = - // Comparator.comparing(ProofScriptArgument::isRequired); - // Comparator byName = - // Comparator.comparing(ProofScriptArgument::getName); - // - // Comparator byPos = Comparator.comparing(it -> { - // if (it.isPositionalVarArgs()) { - // it.getPositionalVarargs().startIndex(); - // } - // if (it.isPositional()) { - // it.getArgument().value(); - // } - // - // return -1; - // }); - // - // - // var comp = optional - // .thenComparing(flagal) - // .thenComparing(positional) - // .thenComparing(allargsal) - // .thenComparing(byRequired) - // .thenComparing(byPos) - // .thenComparing(byName); - var args = Arrays.stream(parameterClazz.getDeclaredFields()) .map(ProofScriptArgument::new) .sorted(Comparator.comparing(ProofScriptArgument::orderString))