Skip to content

Local Variable Type Hints #7892

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 18 commits into
base: dev/feature
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions src/main/java/ch/njol/skript/ScriptLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ch.njol.skript.config.SectionNode;
import ch.njol.skript.config.SimpleNode;
import ch.njol.skript.events.bukkit.PreScriptLoadEvent;
import ch.njol.skript.lang.ExecutionIntent;
import ch.njol.skript.lang.Section;
import ch.njol.skript.lang.SkriptParser;
import ch.njol.skript.lang.Statement;
Expand All @@ -20,7 +21,6 @@
import ch.njol.skript.util.SkriptColor;
import ch.njol.skript.util.Task;
import ch.njol.skript.util.Timespan;
import ch.njol.skript.variables.TypeHints;
import ch.njol.util.NonNullPair;
import ch.njol.util.OpenCloseable;
import ch.njol.util.StringUtils;
Expand Down Expand Up @@ -951,6 +951,10 @@ public static ArrayList<TriggerItem> loadItems(SectionNode node) {

ArrayList<TriggerItem> items = new ArrayList<>();

// Begin local variable type hints
parser.getHintManager().enterScope();
boolean freezeHints = false;

boolean executionStops = false;
for (Node subNode : node) {
parser.setNode(subNode);
Expand Down Expand Up @@ -983,7 +987,6 @@ public static ArrayList<TriggerItem> loadItems(SectionNode node) {

items.add(item);
} else if (subNode instanceof SectionNode subSection) {
TypeHints.enterScope(); // Begin conditional type hints

RetainingLogHandler handler = SkriptLogger.startRetainingLog();
find_section:
Expand Down Expand Up @@ -1023,9 +1026,6 @@ public static ArrayList<TriggerItem> loadItems(SectionNode node) {
}

items.add(item);

// Destroy these conditional type hints
TypeHints.exitScope();
} else {
continue;
}
Expand All @@ -1037,7 +1037,36 @@ public static ArrayList<TriggerItem> loadItems(SectionNode node) {
Skript.warning("Unreachable code. The previous statement stops further execution.");
}
executionStops = item.executionIntent() != null;

if (executionStops && !freezeHints) {
freezeHints = true;

// If we are exiting multiple sections, hints should instead be copied to the section we are exiting to
// Exiting only one section does not require special behavior
if (item.executionIntent() instanceof ExecutionIntent.StopSections intent && intent.levels() > 1) {
parser.getHintManager().mergeScope(0, intent.levels());
parser.getHintManager().enterScope(); // Enter a new scope to capture all new type hints
// Clear scope to prevent duplicate copying
// We clear after entering the capturing scope so that the capturing scope still has hints
parser.getHintManager().clearScope(1);
} else {
parser.getHintManager().enterScope(); // Enter a new scope to capture all new type hints
}
}
}

// If hints were frozen, then we need to destroy the scope in which they were captured
if (freezeHints) {
parser.getHintManager().clearScope(0); // Clear scope to prevent copying upon exit
parser.getHintManager().exitScope();
}
// If the previous section contains a statement that stops the trigger, then any type hints
// provided by the section are not useful. Thus, we clear them.
if (items.stream().anyMatch(item -> item.executionIntent() instanceof ExecutionIntent.StopTrigger)) {
parser.getHintManager().clearScope(0);
}
// Destroy local variable type hints for this section
parser.getHintManager().exitScope();

for (int i = 0; i < items.size() - 1; i++)
items.get(i).setNext(items.get(i + 1));
Expand Down
50 changes: 36 additions & 14 deletions src/main/java/ch/njol/skript/effects/EffChange.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import ch.njol.skript.expressions.ExprParse;
import ch.njol.skript.lang.*;
import ch.njol.skript.lang.parser.ParserInstance;
import ch.njol.skript.variables.HintManager;
import org.skriptlang.skript.lang.script.ScriptWarning;
import org.bukkit.event.Event;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -91,7 +93,7 @@ public class EffChange extends Effect {

@SuppressWarnings({"unchecked", "null"})
@Override
public boolean init(final Expression<?>[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parser) {
public boolean init(final Expression<?>[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) {
mode = patterns.getInfo(matchedPattern);

switch (mode) {
Expand Down Expand Up @@ -234,25 +236,45 @@ else if (mode == ChangeMode.SET)
return false;
}

if (changed instanceof Variable && !changed.isSingle() && mode == ChangeMode.SET) {
if (ch instanceof ExprParse) {
((ExprParse) ch).flatten = false;
} else if (ch instanceof ExpressionList) {
for (Expression<?> expression : ((ExpressionList<?>) ch).getExpressions()) {
if (expression instanceof ExprParse)
((ExprParse) expression).flatten = false;
if (changed instanceof Variable<?> variable) {
// special ExprParse handling
if (!variable.isSingle() && mode == ChangeMode.SET) {
if (ch instanceof ExprParse exprParse) {
exprParse.flatten = false;
} else if (ch instanceof ExpressionList<?> exprList) {
for (Expression<?> expr : exprList.getExpressions()) {
if (expr instanceof ExprParse exprParse) {
exprParse.flatten = false;
}
}
}
}
}

if (changed instanceof Variable && !((Variable<?>) changed).isLocal() && (mode == ChangeMode.SET || ((Variable<?>) changed).isList() && mode == ChangeMode.ADD)) {
final ClassInfo<?> ci = Classes.getSuperClassInfo(ch.getReturnType());
if (ci.getC() != Object.class && ci.getSerializer() == null && ci.getSerializeAs() == null && !SkriptConfig.disableObjectCannotBeSavedWarnings.value()) {
if (getParser().isActive() && !getParser().getCurrentScript().suppressesWarning(ScriptWarning.VARIABLE_SAVE)) {
Skript.warning(ci.getName().withIndefiniteArticle() + " cannot be saved, i.e. the contents of the variable " + changed + " will be lost when the server stops.");
// handling for operations that might update the type(s) of a variable
if (mode == ChangeMode.SET || (variable.isList() && mode == ChangeMode.ADD)) {
if (HintManager.canUseHints(variable)) { // hint handling
HintManager hintManager = getParser().getHintManager();
Class<?>[] hints = ch.possibleReturnTypes();
if (mode == ChangeMode.SET) { // override existing hints in scope
hintManager.set(variable, hints);
} else {
hintManager.add(variable, hints);
}
}
if (!variable.isLocal()) { // global variables: check whether the value can be saved
ClassInfo<?> ci = Classes.getSuperClassInfo(ch.getReturnType());
if (ci.getC() != Object.class && ci.getSerializer() == null && ci.getSerializeAs() == null && !SkriptConfig.disableObjectCannotBeSavedWarnings.value()) {
ParserInstance parser = getParser();
if (parser.isActive() && !parser.getCurrentScript().suppressesWarning(ScriptWarning.VARIABLE_SAVE)) {
Skript.warning(ci.getName().withIndefiniteArticle() + " cannot be saved, i.e. the contents of the variable " + changed + " will be lost when the server stops.");
}
}
}
}
}
} else if (changed instanceof Variable<?> variable && mode == ChangeMode.DELETE && HintManager.canUseHints(variable)) {
// remove type hints in this scope only for a deleted variable
getParser().getHintManager().delete(variable);
}
return true;
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/ch/njol/skript/effects/EffCopy.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.Variable;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.variables.HintManager;
import ch.njol.skript.variables.Variables;
import ch.njol.util.Kleenean;
import org.bukkit.event.Event;
Expand Down Expand Up @@ -64,6 +65,16 @@ public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelaye
return false;
}
}

// set type hints for destination(s) if applicable
Class<?>[] sourceHints = source.possibleReturnTypes();
HintManager hintManager = getParser().getHintManager();
for (Variable<?> destination : destinations) {
if (HintManager.canUseHints(destination)) {
hintManager.set(destination, sourceHints);
}
}

return true;
}

Expand Down
27 changes: 18 additions & 9 deletions src/main/java/ch/njol/skript/effects/EffTransform.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.Variable;
import ch.njol.skript.lang.parser.ParserInstance;
import ch.njol.skript.variables.HintManager;
import ch.njol.skript.variables.Variables;
import ch.njol.util.Kleenean;
import ch.njol.util.Pair;
Expand Down Expand Up @@ -69,19 +70,27 @@ public class EffTransform extends Effect implements InputSource {

@Override
public boolean init(Expression<?>[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
if (expressions[0].isSingle() || !(expressions[0] instanceof Variable)) {
if (parseResult.regexes.isEmpty()) {
return false;
}

if (expressions[0].isSingle() || !(expressions[0] instanceof Variable<?> variable)) {
Skript.error("You can only transform list variables!");
return false;
}
unmappedObjects = (Variable<?>) expressions[0];

//noinspection DuplicatedCode
if (!parseResult.regexes.isEmpty()) {
@Nullable String unparsedExpression = parseResult.regexes.get(0).group();
assert unparsedExpression != null;
mappingExpr = parseExpression(unparsedExpression, getParser(), SkriptParser.ALL_FLAGS);
return mappingExpr != null;
unmappedObjects = variable;

String unparsedExpression = parseResult.regexes.get(0).group();
mappingExpr = parseExpression(unparsedExpression, getParser(), SkriptParser.ALL_FLAGS);
if (mappingExpr == null) {
return false;
}

// type hints
if (HintManager.canUseHints(variable)) {
getParser().getHintManager().set(variable, mappingExpr.possibleReturnTypes());
}

return true;
}

Expand Down
7 changes: 7 additions & 0 deletions src/main/java/ch/njol/skript/expressions/ExprNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ch.njol.skript.Skript;
import ch.njol.skript.classes.Changer.ChangeMode;
import ch.njol.skript.config.EntryNode;
import ch.njol.skript.config.Node;
import ch.njol.skript.config.SectionNode;
import ch.njol.skript.doc.Description;
Expand Down Expand Up @@ -124,6 +125,12 @@ public Class<? extends Node> getReturnType() {
return Node.class;
}

@Override
public Class<? extends Node>[] possibleReturnTypes() {
//noinspection unchecked
return new Class[]{Node.class, EntryNode.class};
}

@Override
public String toString(@Nullable Event event, boolean debug) {
if (isPath)
Expand Down
67 changes: 46 additions & 21 deletions src/main/java/ch/njol/skript/lang/Variable.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import ch.njol.skript.structures.StructVariables.DefaultVariables;
import ch.njol.skript.util.StringMode;
import ch.njol.skript.util.Utils;
import ch.njol.skript.variables.TypeHints;
import ch.njol.skript.variables.Variables;
import ch.njol.util.Kleenean;
import ch.njol.util.Pair;
Expand Down Expand Up @@ -196,41 +195,62 @@ else if (character == '%')

// Check for local variable type hints
if (isLocal && variableString.isSimple()) { // Only variable names we fully know already
Class<?> hint = TypeHints.get(variableString.toString());
if (hint != null && !hint.equals(Object.class)) { // Type hint available
Set<Class<?>> hints = parser.getHintManager().get(variableString.toString(null));
if (!hints.isEmpty()) { // Type hint(s) available
if (types[0] == Object.class) { // Object is generic, so we initialize with the hints instead
//noinspection unchecked, rawtypes
List<Class<? extends T>> variableTypes = new ArrayList<>((Collection) hints);
// TODO don't include Object. Without it, a lot breaks right now.
//noinspection unchecked
variableTypes.add(0, (Class<? extends T>) Object.class);
//noinspection unchecked
return new Variable<>(variableString, variableTypes.toArray(Class[]::new), true, isPlural, null);
}

List<Class<? extends T>> potentialTypes = new ArrayList<>();

// See if we can get correct type without conversion
for (Class<? extends T> type : types) {
assert type != null;
if (type.isAssignableFrom(hint)) {
// Hint matches, use variable with exactly correct type
return new Variable<>(variableString, CollectionUtils.array(type), true, isPlural, null);
// check whether we could resolve to 'type' at runtime
if (hints.stream().anyMatch(hint -> {
// edge case: see Expression#beforeChange
if (hint == ch.njol.skript.util.slot.Slot.class && type == org.bukkit.inventory.ItemStack.class) {
return true;
}
// typically, check whether the types align
return type.isAssignableFrom(hint);
})) {
potentialTypes.add(type);
}
}
if (!potentialTypes.isEmpty()) { // Hint matches, use variable with exactly correct type
//noinspection unchecked
return new Variable<>(variableString, potentialTypes.toArray(Class[]::new), true, isPlural, null);
}

// Or with conversion?
for (Class<? extends T> type : types) {
if (Converters.converterExists(hint, type)) {
// Hint matches, even though converter is needed
return new Variable<>(variableString, CollectionUtils.array(type), true, isPlural, null);
}

// Special cases
if (type.isAssignableFrom(World.class) && hint.isAssignableFrom(String.class)) {
// String->World conversion is weird spaghetti code
return new Variable<>(variableString, types, true, isPlural, null);
} else if (type.isAssignableFrom(Player.class) && hint.isAssignableFrom(String.class)) {
// String->Player conversion is not available at this point
return new Variable<>(variableString, types, true, isPlural, null);
// check whether we could resolve to 'type' at runtime (with conversion)
if (hints.stream().anyMatch(hint -> Converters.converterExists(hint, type))) {
potentialTypes.add(type);
}
}
if (!potentialTypes.isEmpty()) { // Hint matches, even though converter is needed
//noinspection unchecked
return new Variable<>(variableString, potentialTypes.toArray(Class[]::new), true, isPlural, null);
}

// Hint exists and does NOT match any types requested
ClassInfo<?>[] infos = new ClassInfo[types.length];
for (int i = 0; i < types.length; i++) {
infos[i] = Classes.getExactClassInfo(types[i]);
}
Skript.warning("Variable '{_" + name + "}' is " + Classes.toString(Classes.getExactClassInfo(hint))
+ ", not " + Classes.toString(infos, false));
ClassInfo<?>[] hintInfos = hints.stream()
.map(Classes::getExactClassInfo)
.toArray(ClassInfo[]::new);
String isTypes = Utils.a(Classes.toString(hintInfos, false));
String notTypes = Utils.a(Classes.toString(infos, false));
Skript.warning("Expected variable '{_" + variableString.toString(null) + "}' to be " + notTypes + ", but it is " + isTypes);
// Fall back to not having any type hints
}
}
Expand Down Expand Up @@ -261,6 +281,11 @@ public Class<? extends T> getReturnType() {
return superType;
}

@Override
public Class<? extends T>[] possibleReturnTypes() {
return Arrays.copyOf(types, types.length);
}

@Override
public String toString(@Nullable Event event, boolean debug) {
StringBuilder stringBuilder = new StringBuilder()
Expand Down
Loading