diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jass/AntlrJassParseTreeTransformer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jass/AntlrJassParseTreeTransformer.java index dd2dc7c8b..a3d4c8771 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jass/AntlrJassParseTreeTransformer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jass/AntlrJassParseTreeTransformer.java @@ -15,9 +15,11 @@ import org.eclipse.jdt.annotation.Nullable; import java.util.List; +import java.util.Set; public class AntlrJassParseTreeTransformer { + private static final Set JASS_PRIMITIVE_TYPES = Set.of("integer", "real", "boolean"); private final String file; private final ErrorHandler cuErrorHandler; private final LineOffsets lineOffsets; @@ -123,12 +125,23 @@ private WStatements transformJassLocals(List jassLo OptTypeExpr optTyp = transformOptionalType(l.jassTypeExpr()); Identifier name = Ast.Identifier(source(l.name), l.name.getText()); OptExpr initialExpr = transformOptionalExpr(l.initial); + if (l.initial == null && shouldDefaultLocalToNull(optTyp)) { + initialExpr = Ast.ExprNull(source(l)); + } result.add(Ast.LocalVarDef(source(l), modifiers, optTyp, name, initialExpr)); } return result; } + private boolean shouldDefaultLocalToNull(OptTypeExpr optTyp) { + if (!(optTyp instanceof TypeExprSimple)) { + return false; + } + String typeName = ((TypeExprSimple) optTyp).getTypeName(); + return !JASS_PRIMITIVE_TYPES.contains(typeName); + } + private WStatements transformJassStatements(JassParser.JassStatementsContext stmts) { WStatements result = Ast.WStatements(); for (JassParser.JassStatementContext s : stmts.jassStatement()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jurst/AntlrJurstParseTreeTransformer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jurst/AntlrJurstParseTreeTransformer.java index 2a2599de3..5b86a500b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jurst/AntlrJurstParseTreeTransformer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/jurst/AntlrJurstParseTreeTransformer.java @@ -17,8 +17,10 @@ import org.eclipse.jdt.annotation.Nullable; import java.util.List; +import java.util.Set; public class AntlrJurstParseTreeTransformer { + private static final Set JASS_PRIMITIVE_TYPES = Set.of("integer", "real", "boolean"); private final String file; private final ErrorHandler cuErrorHandler; @@ -134,12 +136,23 @@ private WStatements transformJassLocals(List jassLocals) { OptTypeExpr optTyp = transformOptionalType(l.typeExpr()); Identifier name = text(l.name); OptExpr initialExpr = transformOptionalExpr(l.initial); + if (l.initial == null && shouldDefaultJassLocalToNull(optTyp)) { + initialExpr = Ast.ExprNull(source(l)); + } result.add(Ast.LocalVarDef(source(l), modifiers, optTyp, name, initialExpr)); } return result; } + private boolean shouldDefaultJassLocalToNull(OptTypeExpr optTyp) { + if (!(optTyp instanceof TypeExprSimple)) { + return false; + } + String typeName = ((TypeExprSimple) optTyp).getTypeName(); + return !JASS_PRIMITIVE_TYPES.contains(typeName); + } + private WStatements transformJassStatements(JassStatementsContext stmts) { WStatements result = Ast.WStatements(); for (JassStatementContext s : stmts.jassStatement()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java index 3b20d5377..68920c499 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java @@ -27,10 +27,12 @@ import java.util.Comparator; import java.util.Deque; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @SuppressWarnings("Duplicates") public class AntlrWurstParseTreeTransformer { + private static final Set JASS_PRIMITIVE_TYPES = Set.of("integer", "real", "boolean"); private final String file; private final ErrorHandler cuErrorHandler; @@ -225,12 +227,23 @@ private WStatements transformJassLocals(List jassLocals) { OptTypeExpr optTyp = transformOptionalType(l.typeExpr()); Identifier name = text(l.name); OptExpr initialExpr = transformOptionalExpr(l.initial); + if (l.initial == null && shouldDefaultJassLocalToNull(optTyp)) { + initialExpr = Ast.ExprNull(source(l)); + } result.add(Ast.LocalVarDef(source(l), modifiers, optTyp, name, initialExpr)); } return result; } + private boolean shouldDefaultJassLocalToNull(OptTypeExpr optTyp) { + if (!(optTyp instanceof TypeExprSimple)) { + return false; + } + String typeName = ((TypeExprSimple) optTyp).getTypeName(); + return !JASS_PRIMITIVE_TYPES.contains(typeName); + } + private WStatements transformJassStatements(JassStatementsContext stmts) { WStatements result = Ast.WStatements(); if (stmts != null && stmts.jassStatement() != null) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index f01f0dd55..6bad5e815 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1644,6 +1644,125 @@ private void checkUninitializedVars(FunctionLike f) { && !f.getSource().getFile().endsWith("war3map.j")) { new DataflowAnomalyAnalysis(Utils.isJassCode(f)).execute(f); } + checkJassImplicitNullLocalsReadWithoutExplicitWrite(f); + } + + /** + * JASS compatibility shim: we currently synthesize "= null" for uninitialized non-primitive + * locals to avoid invalid emitted JASS. Still report likely user bugs early when such a local + * is read before its first explicit assignment in the input. + */ + private void checkJassImplicitNullLocalsReadWithoutExplicitWrite(FunctionLike f) { + if (!Utils.isJassCode(f)) { + return; + } + + List implicitNullLocals = new ArrayList<>(); + f.accept(new Element.DefaultVisitor() { + @Override + public void visit(LocalVarDef localVarDef) { + super.visit(localVarDef); + if (isImplicitNullInit(localVarDef)) { + implicitNullLocals.add(localVarDef); + } + } + }); + if (implicitNullLocals.isEmpty()) { + return; + } + + Set implicitNullLocalSet = new HashSet<>(implicitNullLocals); + Map firstExplicitWrite = new HashMap<>(); + f.accept(new Element.DefaultVisitor() { + @Override + public void visit(StmtSet stmtSet) { + super.visit(stmtSet); + NameLink link = stmtSet.getUpdatedExpr().attrNameLink(); + if (link != null && link.getDef() instanceof LocalVarDef) { + LocalVarDef local = (LocalVarDef) link.getDef(); + if (!implicitNullLocalSet.contains(local)) { + return; + } + StmtSet previous = firstExplicitWrite.get(local); + if (previous == null + || stmtSet.attrSource().getLeftPos() < previous.attrSource().getLeftPos()) { + firstExplicitWrite.put(local, stmtSet); + } + } + } + }); + + Set readBeforeExplicitWrite = new HashSet<>(); + f.accept(new Element.DefaultVisitor() { + @Override + public void visit(ExprVarAccess varAccess) { + super.visit(varAccess); + NameLink link = varAccess.attrNameLink(); + if (link == null || !(link.getDef() instanceof LocalVarDef)) { + return; + } + LocalVarDef local = (LocalVarDef) link.getDef(); + if (!implicitNullLocalSet.contains(local)) { + return; + } + if (isWriteTarget(varAccess)) { + return; + } + StmtSet firstWrite = firstExplicitWrite.get(local); + if (firstWrite == null) { + readBeforeExplicitWrite.add(local); + return; + } + StmtSet enclosingSet = nearestEnclosingStmtSet(varAccess); + if (enclosingSet == firstWrite) { + readBeforeExplicitWrite.add(local); + return; + } + if (varAccess.attrSource().getLeftPos() < firstWrite.attrSource().getLeftPos()) { + readBeforeExplicitWrite.add(local); + } + } + }); + + for (LocalVarDef local : implicitNullLocals) { + if (readBeforeExplicitWrite.contains(local)) { + local.addWarning("Variable " + local.getName() + + " is read before explicit initialization in input JASS; defaulting to null."); + } + } + } + + private boolean isWriteTarget(ExprVarAccess varAccess) { + if (!(varAccess.getParent() instanceof StmtSet)) { + return false; + } + StmtSet set = (StmtSet) varAccess.getParent(); + return set.getUpdatedExpr() == varAccess; + } + + private @Nullable StmtSet nearestEnclosingStmtSet(Element e) { + Element current = e.getParent(); + while (current != null) { + if (current instanceof StmtSet) { + return (StmtSet) current; + } + current = current.getParent(); + } + return null; + } + + private boolean isImplicitNullInit(LocalVarDef localVarDef) { + if (!(localVarDef.getInitialExpr() instanceof ExprNull)) { + return false; + } + // Synthetic null-inits created by the JASS parser use the exact local declaration source span. + return sameSourceSpan(localVarDef.getInitialExpr().attrSource(), localVarDef.attrSource()); + } + + private boolean sameSourceSpan(de.peeeq.wurstscript.parser.WPos a, de.peeeq.wurstscript.parser.WPos b) { + return a.getFile().equals(b.getFile()) + && a.getLeftPos() == b.getLeftPos() + && a.getRightPos() == b.getRightPos(); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilationUnitTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilationUnitTests.java index 9af003e60..8792bfcb0 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilationUnitTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompilationUnitTests.java @@ -38,4 +38,66 @@ public void jass() { )); } + @Test + public void jassLocalHandleDefaultsToNull() { + testAssertOkLinesWithStdLib(false, + "function tiw takes nothing returns nothing", + "local location uiw", + "set uiw = null", + "endfunction", + "package B", + " init", + " tiw()", + "endpackage" + ); + } + + @Test + public void jassLocalHandleReadWithoutExplicitInitReportsEarly() { + testAssertWarningsLinesWithStdLib(false, "read before explicit initialization in input JASS", + "function tiw takes nothing returns nothing", + "local location uiw", + "call RemoveLocation(uiw)", + "endfunction", + "package B", + " init", + " tiw()", + "endpackage" + ); + } + + @Test + public void war3mapJassLocalHandleReadWithoutExplicitInitReportsEarly() { + test() + .withStdLib() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("read before explicit initialization in input JASS") + .compilationUnits( + compilationUnit("war3map.j", + "function showUnitTextAlliesWithZ takes nothing returns nothing", + "local location p2", + "call RemoveLocation(p2)", + "endfunction" + ) + ); + } + + @Test + public void jassLocalHandleReadBeforeLaterWriteStillWarns() { + testAssertWarningsLinesWithStdLib(false, "read before explicit initialization in input JASS", + "function tiw takes nothing returns nothing", + "local location uiw", + "call RemoveLocation(uiw)", + "set uiw = GetRectCenter(GetPlayableMapRect())", + "call RemoveLocation(uiw)", + "set uiw = null", + "endfunction", + "package B", + " init", + " tiw()", + "endpackage" + ); + } + }