mirror of
https://github.com/apple/pkl.git
synced 2026-04-18 06:29:45 +02:00
SPICE-0025: pkl run CLI framework (#1367)
This commit is contained in:
158
pkl-core/src/main/java/org/pkl/core/CommandSpec.java
Normal file
158
pkl-core/src/main/java/org/pkl/core/CommandSpec.java
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public record CommandSpec(
|
||||
String name,
|
||||
@Nullable String helpText,
|
||||
boolean hidden,
|
||||
boolean noOp,
|
||||
Iterable<Option> options,
|
||||
List<CommandSpec> subcommands,
|
||||
ApplyFunction apply) {
|
||||
|
||||
public sealed interface Option {
|
||||
String name();
|
||||
|
||||
String[] getNames();
|
||||
|
||||
class MissingOption extends RuntimeException {
|
||||
public MissingOption() {}
|
||||
}
|
||||
|
||||
class BadValue extends RuntimeException {
|
||||
public BadValue(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public static BadValue invalid(String value, String type) {
|
||||
return new BadValue(String.format("%s is not a valid %s", value, type));
|
||||
}
|
||||
|
||||
public static BadValue badKeyValue(String value) {
|
||||
return new BadValue(String.format("%s is not a valid key=value pair", value));
|
||||
}
|
||||
|
||||
public static BadValue invalidChoice(String value, List<String> choices) {
|
||||
return new BadValue(
|
||||
String.format(
|
||||
"invalid choice: %s. (choose from %s)", value, String.join(", ", choices)));
|
||||
}
|
||||
|
||||
public static BadValue invalidChoice(String value, String choice) {
|
||||
return new BadValue(String.format("invalid choice: %s. (choose from %s)", value, choice));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static sealed class CompletionCandidates {
|
||||
public static final CompletionCandidates PATH = new StaticCompletionCandidates();
|
||||
|
||||
public static final class Fixed extends CompletionCandidates {
|
||||
private final Set<String> values;
|
||||
|
||||
public Fixed(Set<String> values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
public Set<String> getValues() {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StaticCompletionCandidates extends CompletionCandidates {}
|
||||
}
|
||||
|
||||
public record Flag(
|
||||
String name,
|
||||
@Nullable String helpText,
|
||||
boolean showAsRequired,
|
||||
BiFunction<String, URI, Object> transformEach,
|
||||
Function<List<Object>, Object> transformAll,
|
||||
@Nullable CompletionCandidates completionCandidates,
|
||||
@Nullable String shortName,
|
||||
String metavar,
|
||||
boolean hidden,
|
||||
@Nullable String defaultValue)
|
||||
implements Option {
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return shortName == null
|
||||
? new String[] {"--" + name}
|
||||
: new String[] {"--" + name, "-" + shortName};
|
||||
}
|
||||
}
|
||||
|
||||
public record BooleanFlag(
|
||||
String name,
|
||||
@Nullable String helpText,
|
||||
@Nullable String shortName,
|
||||
boolean hidden,
|
||||
@Nullable Boolean defaultValue)
|
||||
implements Option {
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return shortName == null
|
||||
? new String[] {"--" + name}
|
||||
: new String[] {"--" + name, "-" + shortName};
|
||||
}
|
||||
}
|
||||
|
||||
public record CountedFlag(
|
||||
String name, @Nullable String helpText, @Nullable String shortName, boolean hidden)
|
||||
implements Option {
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return shortName == null
|
||||
? new String[] {"--" + name}
|
||||
: new String[] {"--" + name, "-" + shortName};
|
||||
}
|
||||
}
|
||||
|
||||
public record Argument(
|
||||
String name,
|
||||
@Nullable String helpText,
|
||||
BiFunction<String, URI, Object> transformEach,
|
||||
Function<List<Object>, Object> transformAll,
|
||||
@Nullable CompletionCandidates completionCandidates,
|
||||
boolean repeated)
|
||||
implements Option {
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[] {name};
|
||||
}
|
||||
}
|
||||
|
||||
public interface ApplyFunction {
|
||||
State apply(Map<String, Object> options, @Nullable State parent);
|
||||
}
|
||||
|
||||
public record State(Object contents, Function<Object, Result> reify) {
|
||||
public Result evaluate() {
|
||||
return reify.apply(contents);
|
||||
}
|
||||
}
|
||||
|
||||
public record Result(byte[] outputBytes, Map<String, FileOutput> outputFiles) {}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.pkl.core;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import org.pkl.core.runtime.VmEvalException;
|
||||
|
||||
/**
|
||||
@@ -224,6 +225,20 @@ public interface Evaluator extends AutoCloseable {
|
||||
*/
|
||||
TestResults evaluateTest(ModuleSource moduleSource, boolean overwrite);
|
||||
|
||||
/**
|
||||
* Parses the command module into a spec that describes the CLI options and subcommands.
|
||||
*
|
||||
* <p>This requires that the target module be a {@code "pkl:Command"} instance.
|
||||
*
|
||||
* <p>Unlike other evaluator methods, the resulting {@link CommandSpec} must be handled in a
|
||||
* closure. This is because specs must be applied to parsed CLI options to produce command state
|
||||
* that is eventually evaluated, which must happen in the active context of an evaluator.
|
||||
*
|
||||
* @throws PklException if an error occurs during evaluation
|
||||
* @throws IllegalStateException if this evaluator has already been closed
|
||||
*/
|
||||
void evaluateCommand(ModuleSource moduleSource, Consumer<CommandSpec> run);
|
||||
|
||||
/**
|
||||
* Releases all resources held by this evaluator. If an {@code evaluate} method is currently
|
||||
* executing, this method blocks until cancellation of that execution has completed.
|
||||
|
||||
@@ -20,11 +20,11 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import org.graalvm.polyglot.Context;
|
||||
@@ -40,6 +40,7 @@ import org.pkl.core.packages.PackageResolver;
|
||||
import org.pkl.core.project.DeclaredDependencies;
|
||||
import org.pkl.core.resource.ResourceReader;
|
||||
import org.pkl.core.runtime.BaseModule;
|
||||
import org.pkl.core.runtime.CommandSpecParser;
|
||||
import org.pkl.core.runtime.Identifier;
|
||||
import org.pkl.core.runtime.ModuleResolver;
|
||||
import org.pkl.core.runtime.ResourceManager;
|
||||
@@ -48,8 +49,6 @@ import org.pkl.core.runtime.VmContext;
|
||||
import org.pkl.core.runtime.VmException;
|
||||
import org.pkl.core.runtime.VmExceptionBuilder;
|
||||
import org.pkl.core.runtime.VmLanguage;
|
||||
import org.pkl.core.runtime.VmMapping;
|
||||
import org.pkl.core.runtime.VmNull;
|
||||
import org.pkl.core.runtime.VmPklBinaryEncoder;
|
||||
import org.pkl.core.runtime.VmStackOverflowException;
|
||||
import org.pkl.core.runtime.VmTyped;
|
||||
@@ -148,7 +147,7 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
return VmUtils.readTextProperty(output);
|
||||
});
|
||||
}
|
||||
@@ -157,7 +156,7 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
var vmBytes = VmUtils.readBytesProperty(output);
|
||||
return vmBytes.export();
|
||||
});
|
||||
@@ -168,7 +167,7 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
var value = VmUtils.readMember(output, Identifier.VALUE);
|
||||
if (value instanceof VmValue vmValue) {
|
||||
vmValue.force(false);
|
||||
@@ -183,20 +182,9 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var filesOrNull = VmUtils.readMember(output, Identifier.FILES);
|
||||
if (filesOrNull instanceof VmNull) {
|
||||
return Map.of();
|
||||
}
|
||||
var files = (VmMapping) filesOrNull;
|
||||
var result = new LinkedHashMap<String, FileOutput>();
|
||||
files.forceAndIterateMemberValues(
|
||||
(key, member, value) -> {
|
||||
assert member.isEntry();
|
||||
result.put((String) key, new FileOutputImpl(this, (VmTyped) value));
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
return VmUtils.readFilesProperty(
|
||||
output, (fileOutput) -> new FileOutputImpl(this, fileOutput));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -239,10 +227,10 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
var expressionResult =
|
||||
switch (expression) {
|
||||
case "module" -> module;
|
||||
case "output.text" -> VmUtils.readTextProperty(readModuleOutput(module));
|
||||
case "output.text" -> VmUtils.readTextProperty(VmUtils.readModuleOutput(module));
|
||||
case "output.value" ->
|
||||
VmUtils.readMember(readModuleOutput(module), Identifier.VALUE);
|
||||
case "output.bytes" -> VmUtils.readBytesProperty(readModuleOutput(module));
|
||||
VmUtils.readMember(VmUtils.readModuleOutput(module), Identifier.VALUE);
|
||||
case "output.bytes" -> VmUtils.readBytesProperty(VmUtils.readModuleOutput(module));
|
||||
default ->
|
||||
VmUtils.evaluateExpression(module, expression, securityManager, moduleResolver);
|
||||
};
|
||||
@@ -289,12 +277,27 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluateCommand(ModuleSource moduleSource, Consumer<CommandSpec> run) {
|
||||
doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var commandRunner =
|
||||
new CommandSpecParser(
|
||||
moduleResolver,
|
||||
securityManager,
|
||||
(fileOutput) -> new FileOutputImpl(this, fileOutput));
|
||||
run.accept(commandRunner.parse(module));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T evaluateOutputValueAs(ModuleSource moduleSource, PClassInfo<T> classInfo) {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
var value = VmUtils.readMember(output, Identifier.VALUE);
|
||||
var valueClassInfo = VmUtils.getClass(value).getPClassInfo();
|
||||
if (valueClassInfo.equals(classInfo)) {
|
||||
@@ -367,6 +370,9 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
} catch (VmException e) {
|
||||
handleTimeout(timeoutTask);
|
||||
throw e.toPklException(frameTransformer, color);
|
||||
} catch (PklException e) {
|
||||
// evaluateCommand can throw PklException from the CLI layer, pass them through
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new PklBugException(e);
|
||||
} catch (ExceptionInInitializerError e) {
|
||||
@@ -420,32 +426,6 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
"evaluationTimedOut", (timeout.getSeconds() + timeout.getNano() / 1_000_000_000d)));
|
||||
}
|
||||
|
||||
private VmTyped readModuleOutput(VmTyped module) {
|
||||
var value = VmUtils.readMember(module, Identifier.OUTPUT);
|
||||
if (value instanceof VmTyped typedOutput
|
||||
&& typedOutput.getVmClass().getPClassInfo() == PClassInfo.ModuleOutput) {
|
||||
return typedOutput;
|
||||
}
|
||||
|
||||
var moduleUri = module.getModuleInfo().getModuleKey().getUri();
|
||||
var builder =
|
||||
new VmExceptionBuilder()
|
||||
.evalError(
|
||||
"invalidModuleOutput",
|
||||
"output",
|
||||
PClassInfo.ModuleOutput.getDisplayName(),
|
||||
VmUtils.getClass(value).getPClassInfo().getDisplayName(),
|
||||
moduleUri);
|
||||
var outputMember = module.getMember(Identifier.OUTPUT);
|
||||
assert outputMember != null;
|
||||
var uriOfValueMember = outputMember.getSourceSection().getSource().getURI();
|
||||
// If `output` was explicitly re-assigned, show that in the stack trace.
|
||||
if (!uriOfValueMember.equals(PClassInfo.pklBaseUri)) {
|
||||
builder.withSourceSection(outputMember.getBodySection()).withMemberName("output");
|
||||
}
|
||||
throw builder.build();
|
||||
}
|
||||
|
||||
private VmException moduleOutputValueTypeMismatch(
|
||||
VmTyped module, PClassInfo<?> expectedClassInfo, Object value, VmTyped output) {
|
||||
var moduleUri = module.getModuleInfo().getModuleKey().getUri();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -146,6 +146,10 @@ public final class VmModifier {
|
||||
return (modifiers & (LOCAL | EXTERNAL | ABSTRACT)) != 0;
|
||||
}
|
||||
|
||||
public static boolean isLocalOrExternalOrAbstractOrFixedOrConst(int modifiers) {
|
||||
return (modifiers & (LOCAL | EXTERNAL | ABSTRACT | FIXED | CONST)) != 0;
|
||||
}
|
||||
|
||||
public static boolean isConstOrFixed(int modifiers) {
|
||||
return (modifiers & (CONST | FIXED)) != 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -45,6 +45,10 @@ public abstract class AmendModuleNode extends SpecializedObjectLiteralNode {
|
||||
this.moduleInfo = moduleInfo;
|
||||
}
|
||||
|
||||
public ModuleInfo getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TruffleBoundary
|
||||
protected AmendModuleNode copy(ExpressionNode newParentNode) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.pkl.core.ast.member;
|
||||
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.pkl.core.runtime.Identifier;
|
||||
import org.pkl.core.runtime.VmClass;
|
||||
@@ -53,6 +54,33 @@ public abstract class ClassMember extends Member {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
public List<VmTyped> getAllAnnotations(boolean ascending) {
|
||||
var annotations = new ArrayList<VmTyped>();
|
||||
|
||||
if (ascending) {
|
||||
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
|
||||
var p = clazz.getDeclaredProperty(getName());
|
||||
if (p != null) {
|
||||
annotations.addAll(p.getAnnotations());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
doGetAllAnnotationsDescending(getDeclaringClass(), annotations);
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private void doGetAllAnnotationsDescending(VmClass clazz, List<VmTyped> annotations) {
|
||||
if (clazz.getSuperclass() != null) {
|
||||
doGetAllAnnotationsDescending(clazz.getSuperclass(), annotations);
|
||||
}
|
||||
var p = clazz.getDeclaredProperty(getName());
|
||||
if (p != null) {
|
||||
annotations.addAll(p.getAnnotations());
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the prototype of the class that declares this member. */
|
||||
public final VmTyped getOwner() {
|
||||
return owner;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package org.pkl.core.ast.member;
|
||||
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.pkl.core.Member.SourceLocation;
|
||||
import org.pkl.core.PClass;
|
||||
@@ -54,33 +53,6 @@ public final class ClassProperty extends ClassMember {
|
||||
this.initializer = initializer;
|
||||
}
|
||||
|
||||
public List<VmTyped> getAllAnnotations(boolean ascending) {
|
||||
var annotations = new ArrayList<VmTyped>();
|
||||
|
||||
if (ascending) {
|
||||
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
|
||||
var p = clazz.getDeclaredProperty(getName());
|
||||
if (p != null) {
|
||||
annotations.addAll(p.getAnnotations());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
doGetAllAnnotationsDescending(getDeclaringClass(), annotations);
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private void doGetAllAnnotationsDescending(VmClass clazz, List<VmTyped> annotations) {
|
||||
if (clazz.getSuperclass() != null) {
|
||||
doGetAllAnnotationsDescending(clazz.getSuperclass(), annotations);
|
||||
}
|
||||
var p = clazz.getDeclaredProperty(getName());
|
||||
if (p != null) {
|
||||
annotations.addAll(p.getAnnotations());
|
||||
}
|
||||
}
|
||||
|
||||
public VmSet getAllModifierMirrors() {
|
||||
var mods = 0;
|
||||
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
|
||||
|
||||
@@ -57,6 +57,10 @@ import org.pkl.core.util.Nullable;
|
||||
|
||||
public abstract class TypeNode extends PklNode {
|
||||
|
||||
public interface ClassTypeNode {
|
||||
VmClass getVmClass();
|
||||
}
|
||||
|
||||
protected TypeNode(SourceSection sourceSection) {
|
||||
super(sourceSection);
|
||||
}
|
||||
@@ -402,7 +406,8 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
|
||||
/** The `module` type for a final module. */
|
||||
public static final class FinalModuleTypeNode extends ObjectSlotTypeNode {
|
||||
public static final class FinalModuleTypeNode extends ObjectSlotTypeNode
|
||||
implements ClassTypeNode {
|
||||
private final VmClass moduleClass;
|
||||
|
||||
public FinalModuleTypeNode(SourceSection sourceSection, VmClass moduleClass) {
|
||||
@@ -456,7 +461,8 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
|
||||
/** The `module` type for an open module. */
|
||||
public static final class NonFinalModuleTypeNode extends ObjectSlotTypeNode {
|
||||
public static final class NonFinalModuleTypeNode extends ObjectSlotTypeNode
|
||||
implements ClassTypeNode {
|
||||
private final VmClass moduleClass; // only used by getVmClass()
|
||||
@Child private ExpressionNode getModuleNode;
|
||||
|
||||
@@ -641,7 +647,7 @@ public abstract class TypeNode extends PklNode {
|
||||
* String/Boolean/Int/Float and their supertypes, only `VmValue`s can possibly pass its type
|
||||
* check.
|
||||
*/
|
||||
public static final class FinalClassTypeNode extends ObjectSlotTypeNode {
|
||||
public static final class FinalClassTypeNode extends ObjectSlotTypeNode implements ClassTypeNode {
|
||||
private final VmClass clazz;
|
||||
|
||||
public FinalClassTypeNode(SourceSection sourceSection, VmClass clazz) {
|
||||
@@ -697,7 +703,8 @@ public abstract class TypeNode extends PklNode {
|
||||
* String/Boolean/Int/Float and their supertypes, only {@link VmValue}s can possibly pass its type
|
||||
* check.
|
||||
*/
|
||||
public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode {
|
||||
public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode
|
||||
implements ClassTypeNode {
|
||||
protected final VmClass clazz;
|
||||
|
||||
public NonFinalClassTypeNode(SourceSection sourceSection, VmClass clazz) {
|
||||
@@ -1088,6 +1095,14 @@ public abstract class TypeNode extends PklNode {
|
||||
|
||||
return unionDefault;
|
||||
}
|
||||
|
||||
public Set<String> getStringLiterals() {
|
||||
return stringLiterals;
|
||||
}
|
||||
|
||||
public @Nullable String getUnionDefault() {
|
||||
return unionDefault;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CollectionTypeNode extends ObjectSlotTypeNode {
|
||||
@@ -1421,6 +1436,10 @@ public abstract class TypeNode extends PklNode {
|
||||
return BaseModule.getMapClass();
|
||||
}
|
||||
|
||||
public TypeNode getKeyTypeNode() {
|
||||
return keyTypeNode;
|
||||
}
|
||||
|
||||
public TypeNode getValueTypeNode() {
|
||||
return valueTypeNode;
|
||||
}
|
||||
@@ -2131,6 +2150,14 @@ public abstract class TypeNode extends PklNode {
|
||||
protected boolean isParametric() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public TypeNode getFirstTypeNode() {
|
||||
return firstTypeNode;
|
||||
}
|
||||
|
||||
public TypeNode getSecondTypeNode() {
|
||||
return secondTypeNode;
|
||||
}
|
||||
}
|
||||
|
||||
public static class VarArgsTypeNode extends ObjectSlotTypeNode {
|
||||
@@ -2313,6 +2340,10 @@ public abstract class TypeNode extends PklNode {
|
||||
protected final boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) {
|
||||
return consumer.accept(this);
|
||||
}
|
||||
|
||||
public long getMask() {
|
||||
return mask;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class UIntTypeAliasTypeNode extends IntMaskSlotTypeNode {
|
||||
@@ -2505,6 +2536,10 @@ public abstract class TypeNode extends PklNode {
|
||||
aliasedTypeNode = typeAlias.instantiate(typeArgumentNodes);
|
||||
}
|
||||
|
||||
public TypeNode getAliasedTypeNode() {
|
||||
return aliasedTypeNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameSlotKind getFrameSlotKind() {
|
||||
return aliasedTypeNode.getFrameSlotKind();
|
||||
@@ -2670,6 +2705,10 @@ public abstract class TypeNode extends PklNode {
|
||||
this.constraintNodes = constraintNodes;
|
||||
}
|
||||
|
||||
public TypeNode getChildTypeNode() {
|
||||
return childNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameSlotKind getFrameSlotKind() {
|
||||
return childNode.getFrameSlotKind();
|
||||
|
||||
@@ -227,10 +227,26 @@ public final class BaseModule extends StdLibModule {
|
||||
return MixinTypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getUIntTypeAlias() {
|
||||
return UIntTypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getUInt8TypeAlias() {
|
||||
return UInt8TypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getUInt16TypeAlias() {
|
||||
return UInt16TypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getUInt32TypeAlias() {
|
||||
return UInt32TypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getCharTypeAlias() {
|
||||
return CharTypeAlias.instance;
|
||||
}
|
||||
|
||||
private static final class AnyClass {
|
||||
static final VmClass instance = loadClass("Any");
|
||||
}
|
||||
@@ -403,10 +419,26 @@ public final class BaseModule extends StdLibModule {
|
||||
static final VmTypeAlias instance = loadTypeAlias("Int32");
|
||||
}
|
||||
|
||||
private static final class UIntTypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("UInt");
|
||||
}
|
||||
|
||||
private static final class UInt8TypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("UInt8");
|
||||
}
|
||||
|
||||
private static final class UInt16TypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("UInt16");
|
||||
}
|
||||
|
||||
private static final class UInt32TypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("UInt32");
|
||||
}
|
||||
|
||||
private static final class CharTypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("Char");
|
||||
}
|
||||
|
||||
private static final class MixinTypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("Mixin");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.runtime;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import java.net.URI;
|
||||
|
||||
public final class CommandModule extends StdLibModule {
|
||||
static final VmTyped instance = VmUtils.createEmptyModule();
|
||||
|
||||
static {
|
||||
loadModule(URI.create("pkl:Command"), instance);
|
||||
}
|
||||
|
||||
private CommandModule() {}
|
||||
|
||||
public static VmTyped getModule() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static VmClass getCommandInfoClass() {
|
||||
return CommandInfoClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getBaseFlagClass() {
|
||||
return BaseFlagClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getFlagClass() {
|
||||
return FlagClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getBooleanFlagClass() {
|
||||
return BooleanFlagClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getCountedFlagClass() {
|
||||
return CountedFlagClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getArgumentClass() {
|
||||
return ArgumentClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getImportClass() {
|
||||
return ImportClass.instance;
|
||||
}
|
||||
|
||||
private static final class CommandInfoClass {
|
||||
static final VmClass instance = loadClass("CommandInfo");
|
||||
}
|
||||
|
||||
private static final class BaseFlagClass {
|
||||
static final VmClass instance = loadClass("BaseFlag");
|
||||
}
|
||||
|
||||
private static final class FlagClass {
|
||||
static final VmClass instance = loadClass("Flag");
|
||||
}
|
||||
|
||||
private static final class BooleanFlagClass {
|
||||
static final VmClass instance = loadClass("BooleanFlag");
|
||||
}
|
||||
|
||||
private static final class CountedFlagClass {
|
||||
static final VmClass instance = loadClass("CountedFlag");
|
||||
}
|
||||
|
||||
private static final class ArgumentClass {
|
||||
static final VmClass instance = loadClass("Argument");
|
||||
}
|
||||
|
||||
private static final class ImportClass {
|
||||
static final VmClass instance = loadClass("Import");
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
private static VmClass loadClass(String className) {
|
||||
var theModule = getModule();
|
||||
return (VmClass) VmUtils.readMember(theModule, Identifier.get(className));
|
||||
}
|
||||
}
|
||||
1214
pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java
Normal file
1214
pkl-core/src/main/java/org/pkl/core/runtime/CommandSpecParser.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -147,6 +147,22 @@ public final class Identifier implements Comparable<Identifier> {
|
||||
// members of pkl.yaml
|
||||
public static final Identifier MAX_COLLECTION_ALIASES = get("maxCollectionAliases");
|
||||
|
||||
// members of pkl.Command
|
||||
public static final Identifier OPTIONS = get("options");
|
||||
public static final Identifier PARENT = get("parent");
|
||||
public static final Identifier COMMAND = get("command");
|
||||
public static final Identifier DESCRIPTION = get("description");
|
||||
public static final Identifier HIDE = get("hide");
|
||||
public static final Identifier NOOP = get("noOp");
|
||||
public static final Identifier SUBCOMMANDS = get("subcommands");
|
||||
public static final Identifier SHORT_NAME = get("shortName");
|
||||
public static final Identifier METAVAR = get("metavar");
|
||||
public static final Identifier MULTIPLE = get("multiple");
|
||||
public static final Identifier CONVERT = get("convert");
|
||||
public static final Identifier TRANSFORM_ALL = get("transformAll");
|
||||
public static final Identifier GLOB = get("glob");
|
||||
public static final Identifier COMPLETION_CANDIDATES = get("completionCandidates");
|
||||
|
||||
// common in lambdas etc
|
||||
public static final Identifier IT = get("it");
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ public final class ModuleCache {
|
||||
return BaseModule.getModule();
|
||||
case "Benchmark":
|
||||
return BenchmarkModule.getModule();
|
||||
case "Command":
|
||||
return CommandModule.getModule();
|
||||
case "jsonnet":
|
||||
return JsonnetModule.getModule();
|
||||
case "math":
|
||||
|
||||
@@ -69,7 +69,7 @@ public final class TestRunner {
|
||||
var resultsBuilder = new TestResults.Builder(info.getModuleName(), getDisplayUri(info));
|
||||
|
||||
try {
|
||||
checkAmendsPklTest(testModule);
|
||||
VmUtils.checkAmends(testModule, TestModule.getModule().getVmClass());
|
||||
} catch (VmException v) {
|
||||
var error =
|
||||
new TestResults.Error(v.getMessage(), v.toPklException(stackFrameTransformer, useColor));
|
||||
@@ -83,17 +83,6 @@ public final class TestRunner {
|
||||
return resultsBuilder.build();
|
||||
}
|
||||
|
||||
private void checkAmendsPklTest(VmTyped value) {
|
||||
var testModuleClass = TestModule.getModule().getVmClass();
|
||||
var moduleClass = value.getVmClass();
|
||||
while (moduleClass != testModuleClass) {
|
||||
moduleClass = moduleClass.getSuperclass();
|
||||
if (moduleClass == null) {
|
||||
throw new VmExceptionBuilder().typeMismatch(value, testModuleClass).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TestSectionResults runFacts(VmTyped testModule) {
|
||||
var facts = VmUtils.readMember(testModule, Identifier.FACTS);
|
||||
if (facts instanceof VmNull) return new TestSectionResults(TestSectionName.FACTS, List.of());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -30,12 +30,14 @@ import java.net.URI;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
import java.util.stream.Collectors;
|
||||
import org.graalvm.polyglot.Context;
|
||||
import org.graalvm.polyglot.Engine;
|
||||
import org.organicdesign.fp.collections.ImMap;
|
||||
import org.pkl.core.FileOutput;
|
||||
import org.pkl.core.PClassInfo;
|
||||
import org.pkl.core.PObject;
|
||||
import org.pkl.core.SecurityManager;
|
||||
@@ -179,6 +181,49 @@ public final class VmUtils {
|
||||
return (VmBytes) VmUtils.readMember(receiver, Identifier.BYTES);
|
||||
}
|
||||
|
||||
public static VmTyped readModuleOutput(VmTyped module) {
|
||||
var value = VmUtils.readMember(module, Identifier.OUTPUT);
|
||||
if (value instanceof VmTyped typedOutput
|
||||
&& typedOutput.getVmClass().getPClassInfo() == PClassInfo.ModuleOutput) {
|
||||
return typedOutput;
|
||||
}
|
||||
|
||||
var moduleUri = module.getModuleInfo().getModuleKey().getUri();
|
||||
var builder =
|
||||
new VmExceptionBuilder()
|
||||
.evalError(
|
||||
"invalidModuleOutput",
|
||||
"output",
|
||||
PClassInfo.ModuleOutput.getDisplayName(),
|
||||
VmUtils.getClass(value).getPClassInfo().getDisplayName(),
|
||||
moduleUri);
|
||||
var outputMember = module.getMember(Identifier.OUTPUT);
|
||||
assert outputMember != null;
|
||||
var uriOfValueMember = outputMember.getSourceSection().getSource().getURI();
|
||||
// If `output` was explicitly re-assigned, show that in the stack trace.
|
||||
if (!uriOfValueMember.equals(PClassInfo.pklBaseUri)) {
|
||||
builder.withSourceSection(outputMember.getBodySection()).withMemberName("output");
|
||||
}
|
||||
throw builder.build();
|
||||
}
|
||||
|
||||
public static Map<String, FileOutput> readFilesProperty(
|
||||
VmObjectLike receiver, Function<VmTyped, FileOutput> fileOutputFactory) {
|
||||
var filesOrNull = VmUtils.readMember(receiver, Identifier.FILES);
|
||||
if (filesOrNull instanceof VmNull) {
|
||||
return Map.of();
|
||||
}
|
||||
var files = (VmMapping) filesOrNull;
|
||||
var result = new LinkedHashMap<String, FileOutput>();
|
||||
files.forceAndIterateMemberValues(
|
||||
(key, member, value) -> {
|
||||
assert member.isEntry();
|
||||
result.put((String) key, fileOutputFactory.apply((VmTyped) value));
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
public static Object readMember(VmObjectLike receiver, Object memberKey) {
|
||||
var result = readMemberOrNull(receiver, memberKey);
|
||||
@@ -855,7 +900,7 @@ public final class VmUtils {
|
||||
String expression,
|
||||
SecurityManager securityManager,
|
||||
ModuleResolver moduleResolver) {
|
||||
var syntheticModule = ModuleKeys.synthetic(URI.create(REPL_TEXT), expression);
|
||||
var syntheticModule = ModuleKeys.synthetic(REPL_TEXT_URI, expression);
|
||||
ResolvedModuleKey resolvedModule;
|
||||
try {
|
||||
resolvedModule = syntheticModule.resolve(securityManager);
|
||||
@@ -870,7 +915,7 @@ public final class VmUtils {
|
||||
source.createSection(0, source.getLength()),
|
||||
VmUtils.unavailableSourceSection(),
|
||||
null,
|
||||
"repl:text",
|
||||
REPL_TEXT,
|
||||
syntheticModule,
|
||||
resolvedModule,
|
||||
false);
|
||||
@@ -914,4 +959,20 @@ public final class VmUtils {
|
||||
public static String concat(String str1, String str2) {
|
||||
return str1 + str2;
|
||||
}
|
||||
|
||||
/** Check that a value is a VmTyped and that it inherits from the given class */
|
||||
public static VmTyped checkAmends(Object value, VmClass clazz) {
|
||||
if (!(value instanceof VmTyped typed)) {
|
||||
throw new VmExceptionBuilder().typeMismatch(value, clazz).build();
|
||||
}
|
||||
return checkAmends(typed, clazz);
|
||||
}
|
||||
|
||||
/** Check that a typed value inherits from the given class */
|
||||
public static VmTyped checkAmends(VmTyped value, VmClass clazz) {
|
||||
if (!value.getVmClass().isSubclassOf(clazz)) {
|
||||
throw new VmExceptionBuilder().typeMismatch(value.getVmClass(), clazz).build();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -51,14 +51,14 @@ public final class MathNodes {
|
||||
public abstract static class minInt8 extends ExternalPropertyNode {
|
||||
@Specialization
|
||||
protected long eval(VmTyped self) {
|
||||
return -128;
|
||||
return Byte.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class maxInt8 extends ExternalPropertyNode {
|
||||
@Specialization
|
||||
protected long eval(VmTyped self) {
|
||||
return 127;
|
||||
return Byte.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -471,13 +471,15 @@ public final class GlobResolver {
|
||||
* Resolves a glob expression.
|
||||
*
|
||||
* <p>Each pair is the expanded form of the glob pattern, paired with its resolved absolute URI.
|
||||
*
|
||||
* <p>globPattern must be absolute if enclosing module details are null.
|
||||
*/
|
||||
@TruffleBoundary
|
||||
public static Map<String, ResolvedGlobElement> resolveGlob(
|
||||
SecurityManager securityManager,
|
||||
ReaderBase reader,
|
||||
ModuleKey enclosingModuleKey,
|
||||
URI enclosingUri,
|
||||
@Nullable ModuleKey enclosingModuleKey,
|
||||
@Nullable URI enclosingUri,
|
||||
String globPattern)
|
||||
throws IOException,
|
||||
SecurityManagerException,
|
||||
@@ -486,6 +488,10 @@ public final class GlobResolver {
|
||||
|
||||
var result = new LinkedHashMap<String, ResolvedGlobElement>();
|
||||
var hasAbsoluteGlob = globPattern.matches("\\w+:.*");
|
||||
if ((enclosingModuleKey == null || enclosingUri == null) && !hasAbsoluteGlob) {
|
||||
throw new PklBugException(
|
||||
"GlobResolver.resolveGlob() callers must check that the glob pattern is absolute if calling with null enclosing module info");
|
||||
}
|
||||
|
||||
if (reader.hasHierarchicalUris()) {
|
||||
var splitPattern = splitGlobPatternIntoBaseAndWildcards(reader, globPattern, hasAbsoluteGlob);
|
||||
@@ -493,7 +499,10 @@ public final class GlobResolver {
|
||||
var globParts = splitPattern.second;
|
||||
// short-circuit for glob pattern with no wildcards (can only match 0 or 1 element)
|
||||
if (globParts.length == 0) {
|
||||
var resolvedUri = IoUtils.resolve(reader, enclosingUri, globPattern);
|
||||
var resolvedUri =
|
||||
enclosingUri == null
|
||||
? URI.create(globPattern)
|
||||
: IoUtils.resolve(reader, enclosingUri, globPattern);
|
||||
if (reader.hasElement(securityManager, resolvedUri)) {
|
||||
result.put(globPattern, new ResolvedGlobElement(globPattern, resolvedUri, true));
|
||||
}
|
||||
@@ -501,7 +510,10 @@ public final class GlobResolver {
|
||||
}
|
||||
URI baseUri;
|
||||
try {
|
||||
baseUri = IoUtils.resolve(securityManager, enclosingModuleKey, URI.create(basePath));
|
||||
baseUri =
|
||||
enclosingModuleKey == null
|
||||
? URI.create(basePath)
|
||||
: IoUtils.resolve(securityManager, enclosingModuleKey, URI.create(basePath));
|
||||
} catch (URISyntaxException e) {
|
||||
// assertion: this is only thrown if the pattern starts with a triple-dot import.
|
||||
// the language will throw an error if glob imports is combined with triple-dots.
|
||||
|
||||
@@ -1076,3 +1076,54 @@ invalidStringBase64=\
|
||||
|
||||
characterCodingException=\
|
||||
Invalid bytes for charset "{0}".
|
||||
|
||||
commandSubcommandConflict=\
|
||||
Command `{0}` has subcommands with conflicting name "{1}".\n\
|
||||
Elements of `command.subcommands` must have unique `name` values.
|
||||
|
||||
commandMustNotAssignOrAmendProperty=\
|
||||
Commands must not assign or amend property `{0}`.
|
||||
|
||||
commandOptionNoTypeAnnotation=\
|
||||
No type annotation found for `{0}` property.
|
||||
|
||||
commandOptionsTypeNotClass=\
|
||||
Type annotation `{0}` on `options` property in `pkl:Command` subclass must be a class type.
|
||||
|
||||
commandOptionsTypeAbstractClass=\
|
||||
Command options class `{0}` may not be abstract.
|
||||
|
||||
commandOptionBothFlagAndArgument=\
|
||||
Found both `@Flag` and `@Argument` annotations for options property `{0}`.\n\
|
||||
\n\
|
||||
Only one option type may be specified.
|
||||
|
||||
commandOptionTypeNullableWithDefaultValue=\
|
||||
Unexpected option property `{0}` with nullable type and default value.\n\
|
||||
\n\
|
||||
Options with default values must not be nullable.
|
||||
|
||||
commandOptionUnsupportedType=\
|
||||
Command option property `{0}` has unsupported {1}type `{2}`.
|
||||
|
||||
commandOptionUnexpectedDefaultValue=\
|
||||
Unexpected default value for `@{1}` property `{0}`.\n\
|
||||
\n\
|
||||
{1}s may not specify a default value.
|
||||
|
||||
commandArgumentUnexpectedNonRepeatedNullableType=\
|
||||
Unexpected nullable type for non-collection `@Argument` property `{0}`.\n\
|
||||
\n\
|
||||
Arguments may not be nullable.
|
||||
|
||||
commandArgumentsMultipleRepeated=\
|
||||
More than one repeated option annotated with `@Argument` found: `{0}` and `{1}`.\n\
|
||||
\n\
|
||||
Only one repeated argument is permitted per command.
|
||||
|
||||
commandFlagHelpCollision=\
|
||||
Flag option `{0}` may not have name "help" or short name "h".
|
||||
|
||||
commandFlagInvalidType=\
|
||||
Option `{0}` with annotation `@{1}` has invalid type `{2}`.\n\
|
||||
Expected type: `{3}`
|
||||
|
||||
@@ -9,6 +9,7 @@ Available standard library modules:
|
||||
pkl:analyze
|
||||
pkl:base
|
||||
pkl:Benchmark
|
||||
pkl:Command
|
||||
pkl:DocPackageInfo
|
||||
pkl:DocsiteInfo
|
||||
pkl:EvaluatorSettings
|
||||
|
||||
@@ -0,0 +1,791 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.runtime
|
||||
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createParentDirectories
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.pkl.commons.writeString
|
||||
import org.pkl.core.CommandSpec
|
||||
import org.pkl.core.Evaluator
|
||||
import org.pkl.core.ModuleSource.uri
|
||||
import org.pkl.core.PklException
|
||||
|
||||
class CommandSpecParserTest {
|
||||
companion object {
|
||||
private val renderOptions =
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
import "pkl:Command"
|
||||
|
||||
options: Options
|
||||
|
||||
output {
|
||||
value = options
|
||||
}
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
|
||||
private val evaluator = Evaluator.preconfigured()
|
||||
}
|
||||
|
||||
@TempDir private lateinit var tempDir: Path
|
||||
|
||||
private fun writePklFile(fileName: String, contents: String): URI {
|
||||
tempDir.resolve(fileName).createParentDirectories()
|
||||
return tempDir.resolve(fileName).writeString(contents).toUri()
|
||||
}
|
||||
|
||||
private fun parse(moduleUri: URI): CommandSpec {
|
||||
var spec: CommandSpec? = null
|
||||
evaluator.evaluateCommand(uri(moduleUri)) { spec = it }
|
||||
return spec!!
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `command module does not amend pkl_Command`() {
|
||||
val moduleUri = writePklFile("cmd.pkl", "")
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("Expected value of type `pkl.Command`, but got type")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options property assigned`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
options = new {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("options = ")
|
||||
assertThat(exc.message).contains("Commands must not assign or amend property `options`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options property amended`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
options {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("options {")
|
||||
assertThat(exc.message).contains("Commands must not assign or amend property `options`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parent property assigned`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
parent = new {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("parent = ")
|
||||
assertThat(exc.message).contains("Commands must not assign or amend property `parent`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parent property amended`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
parent {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("parent {")
|
||||
assertThat(exc.message).contains("Commands must not assign or amend property `parent`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options type annotation does not reference class`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
options: "nope" | "try again"
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("options: \"nope\" | \"try again\"")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Type annotation `\"nope\" | \"try again\"` on `options` property in `pkl:Command` subclass must be a class type."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options class is abstract`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
options: Options
|
||||
abstract class Options {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("abstract class Options {")
|
||||
assertThat(exc.message).contains("Command options class `cmd#Options` may not be abstract.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `command property value does not amend CommandInfo`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
command = new Foo {}
|
||||
class Foo
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("command = new Foo {}")
|
||||
assertThat(exc.message)
|
||||
.contains("Expected value of type `pkl.Command#CommandInfo`, but got type `cmd#Foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `first annotation of the same type wins`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
open class BaseOptions {
|
||||
/// foo in BaseOptions
|
||||
@Flag { shortName = "a" }
|
||||
foo: String
|
||||
|
||||
/// bar in BaseOptions
|
||||
@Flag { shortName = "b" }
|
||||
bar: String
|
||||
}
|
||||
class Options extends BaseOptions {
|
||||
/// bar in Options
|
||||
@Flag { shortName = "x" }
|
||||
bar: String
|
||||
|
||||
/// baz in Options
|
||||
@Flag { shortName = "y" }
|
||||
@CountedFlag { shortName = "z" }
|
||||
baz: Int
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val spec = parse(moduleUri)
|
||||
|
||||
// assert class overrides its superclass
|
||||
assertThat(spec.options.toList()[0]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[0] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("bar")
|
||||
assertThat(this.shortName).isEqualTo("x")
|
||||
assertThat(this.helpText).isEqualTo("bar in Options")
|
||||
}
|
||||
|
||||
// assert first flag annotation wins
|
||||
assertThat(spec.options.toList()[1]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[1] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("baz")
|
||||
assertThat(this.shortName).isEqualTo("y")
|
||||
assertThat(this.helpText).isEqualTo("baz in Options")
|
||||
}
|
||||
|
||||
// assert superclass options are inherited
|
||||
assertThat(spec.options.toList()[2]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[2] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("foo")
|
||||
assertThat(this.shortName).isEqualTo("a")
|
||||
assertThat(this.helpText).isEqualTo("foo in BaseOptions")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@Flag and @Argument on the same option`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Flag
|
||||
@Argument
|
||||
foo: String
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String")
|
||||
assertThat(exc.message)
|
||||
.contains("Found both `@Flag` and `@Argument` annotations for options property `foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option with no type annotation`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo = "bar"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo = \"bar\"")
|
||||
assertThat(exc.message).contains("No type annotation found for `foo` property.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nullable option with default not allowed`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: String? = "bar"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String? = \"bar\"")
|
||||
assertThat(exc.message)
|
||||
.contains("Unexpected option property `foo` with nullable type and default value")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option with union type containing non-string-literals`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: "oops" | String
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: \"oops\" | String")
|
||||
assertThat(exc.message)
|
||||
.contains("Command option property `foo` has unsupported type `\"oops\" | String`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `argument with default not allowed`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Argument
|
||||
foo: String = "bar"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String = \"bar\"")
|
||||
assertThat(exc.message).contains("Unexpected default value for `@Argument` property `foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nullable non-collection argument not allowed`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Argument
|
||||
foo: String?
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String?")
|
||||
assertThat(exc.message)
|
||||
.contains("Unexpected nullable type for non-collection `@Argument` property `foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-constant default values result in an optional flag with no default`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: String = "hi"
|
||||
bar: String = foo
|
||||
baz: Map<String, String> = Map()
|
||||
qux: Map<String, String> = baz
|
||||
quux: Int = 5
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val spec = parse(moduleUri)
|
||||
|
||||
assertThat(spec.options.toList()[0]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[0] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("foo")
|
||||
assertThat(this.defaultValue).isEqualTo("hi")
|
||||
}
|
||||
|
||||
assertThat(spec.options.toList()[1]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[1] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("bar")
|
||||
assertThat(this.defaultValue).isNull()
|
||||
}
|
||||
|
||||
assertThat(spec.options.toList()[2]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[2] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("baz")
|
||||
assertThat(this.defaultValue).isNull()
|
||||
}
|
||||
|
||||
assertThat(spec.options.toList()[3]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[3] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("qux")
|
||||
assertThat(this.defaultValue).isNull()
|
||||
}
|
||||
|
||||
assertThat(spec.options.toList()[4]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[4] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("quux")
|
||||
assertThat(this.defaultValue).isEqualTo("5")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flag with collision on --help`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
help: Boolean
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("help: Boolean")
|
||||
assertThat(exc.message)
|
||||
.contains("Flag option `help` may not have name \"help\" or short name \"h\".")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flag with collision on -h`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Flag { shortName = "h" }
|
||||
showHelp: Boolean
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("showHelp: Boolean")
|
||||
assertThat(exc.message)
|
||||
.contains("Flag option `showHelp` may not have name \"help\" or short name \"h\".")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple arguments with collection types not allowed`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Argument
|
||||
list: List<String>
|
||||
@Argument
|
||||
set: Set<String>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("class Options {")
|
||||
assertThat(exc.message)
|
||||
.contains("More than one repeated option annotated with `@Argument` found: `list` and `set`.")
|
||||
assertThat(exc.message).contains("Only one repeated argument is permitted per command.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `collection option with collection element type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: List<List<"a" | "b">>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: List<List<\"a\" | \"b\">>")
|
||||
assertThat(exc.message)
|
||||
.contains("Command option property `foo` has unsupported element type `List<\"a\" | \"b\">`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `collection option with map element type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: List<Map<String, "a" | "b">>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: List<Map<String, \"a\" | \"b\">>")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Command option property `foo` has unsupported element type `Map<String, \"a\" | \"b\">`."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with collection value type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map<String, List<"a" | "b">>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map<String, List<\"a\" | \"b\">>")
|
||||
assertThat(exc.message)
|
||||
.contains("Command option property `foo` has unsupported value type `List<\"a\" | \"b\">`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with map value type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map<String, Map<String, "a" | "b">>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map<String, Map<String, \"a\" | \"b\">>")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Command option property `foo` has unsupported value type `Map<String, \"a\" | \"b\">`."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with collection key type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map<Map<String, "a" | "b">, String>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map<Map<String, \"a\" | \"b\">, String>")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Command option property `foo` has unsupported key type `Map<String, \"a\" | \"b\">`."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with map key type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map<Map<String, "a" | "b">, String>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map<Map<String, \"a\" | \"b\">, String>")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Command option property `foo` has unsupported key type `Map<String, \"a\" | \"b\">`."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with map key type allowed with convert`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Flag { convert = (it) -> Pair("foo", "a") }
|
||||
foo: Map<Map<String, "a" | "b">, String>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
assertDoesNotThrow { parse(moduleUri) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unsupported option type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Foo
|
||||
}
|
||||
class Foo
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Foo")
|
||||
assertThat(exc.message).contains("Command option property `foo` has unsupported type `Foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options constraints in all positions are erased`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
a: String(true)
|
||||
b: String?(true)
|
||||
c: String(true)?
|
||||
d: List<String(true)>
|
||||
e: List<String(true)>(true)
|
||||
f: List<String(true)>(true)?(true)
|
||||
g: (Map<String(true), String(true)>(true)?(true))(true)
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
parse(moduleUri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `conflicting subcommand names`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
import "pkl:Command"
|
||||
command {
|
||||
subcommands {
|
||||
new Sub { command { name = "foo" } }
|
||||
new Sub { command { name = "foo" } }
|
||||
}
|
||||
}
|
||||
|
||||
class Sub extends Command
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("Command `cmd` has subcommands with conflicting name \"foo\".")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `list or set option with no type arguments`() {
|
||||
for (type in listOf("List", "Set")) {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd_$type.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: $type
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: $type")
|
||||
assertThat(exc.message)
|
||||
.contains("Command option property `foo` has unsupported type `$type`.")
|
||||
assertThat(exc.message).contains("$type options must provide one type argument.")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with no type arguments`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map")
|
||||
assertThat(exc.message).contains("Command option property `foo` has unsupported type `Map`.")
|
||||
assertThat(exc.message).contains("Map options must provide two type arguments.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `boolean flag with incorrect type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@BooleanFlag
|
||||
foo: String
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String")
|
||||
assertThat(exc.message)
|
||||
.contains("Option `foo` with annotation `@BooleanFlag` has invalid type `String`.")
|
||||
assertThat(exc.message).contains("Expected type: `Boolean`")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `counted flag with incorrect type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@CountedFlag
|
||||
foo: String
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String")
|
||||
assertThat(exc.message)
|
||||
.contains("Option `foo` with annotation `@CountedFlag` has invalid type `String`.")
|
||||
assertThat(exc.message).contains("Expected type: `Int`")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user