diff --git a/basex-core/src/main/java/org/basex/query/QueryParser.java b/basex-core/src/main/java/org/basex/query/QueryParser.java index 504a0a3ebf..ec47186318 100644 --- a/basex-core/src/main/java/org/basex/query/QueryParser.java +++ b/basex-core/src/main/java/org/basex/query/QueryParser.java @@ -293,9 +293,10 @@ private void check(final MainModule main) throws QueryException { } } - // check function calls and variable references + // check function calls qc.functions.check(qc); - qc.vars.check(); + // resolve variable references + qc.vars.resolve(); if(qc.updating) { // check updating semantics if updating expressions exist @@ -897,8 +898,6 @@ private void contextValueDecl() throws QueryException { */ private void varDecl(final AnnList anns) throws QueryException { final Var var = newVar(); - if(sc.module != null && !eq(var.name.uri(), sc.module.uri())) throw error(MODULENS_X, var); - localVars.pushContext(false); final boolean external = wsConsumeWs(EXTERNAL); Expr expr = null; @@ -909,7 +908,7 @@ private void varDecl(final AnnList anns) throws QueryException { } final VarScope vs = localVars.popContext(); final String doc = docBuilder.toString(); - final StaticVar sv = qc.vars.declare(var, expr, anns, external, vs, doc); + final StaticVar sv = qc.vars.declare(var, moduleURIs, expr, anns, external, vs, doc); vars.add(sv); } diff --git a/basex-core/src/main/java/org/basex/query/func/Records.java b/basex-core/src/main/java/org/basex/query/func/Records.java index f98635674b..09a2279959 100644 --- a/basex-core/src/main/java/org/basex/query/func/Records.java +++ b/basex-core/src/main/java/org/basex/query/func/Records.java @@ -81,7 +81,7 @@ public enum Records { add("members", true, FuncType.get(stp.seqType(Occ.ZERO_OR_MORE)).seqType()). add("simple-content-type", true, FuncType.get(stp.seqType()).seqType()). add("matches", true, FuncType.get(Types.BOOLEAN_O, Types.ANY_ATOMIC_TYPE_O).seqType()). - add("constructo", true, FuncType.get(Types.ANY_ATOMIC_TYPE_ZM, + add("constructor", true, FuncType.get(Types.ANY_ATOMIC_TYPE_ZM, Types.ANY_ATOMIC_TYPE_ZO).seqType()); } diff --git a/basex-core/src/main/java/org/basex/query/util/parse/LocalVars.java b/basex-core/src/main/java/org/basex/query/util/parse/LocalVars.java index 26855447d5..4f5c179665 100644 --- a/basex-core/src/main/java/org/basex/query/util/parse/LocalVars.java +++ b/basex-core/src/main/java/org/basex/query/util/parse/LocalVars.java @@ -1,8 +1,5 @@ package org.basex.query.util.parse; -import static org.basex.query.QueryError.*; -import static org.basex.util.Token.*; - import java.util.*; import org.basex.query.*; @@ -88,25 +85,12 @@ public VarRef resolveLocal(final QNm name, final InputInfo info) { * @param name variable name * @param info input info (can be {@code null}) * @return referenced variable - * @throws QueryException if the variable is not defined */ - public ParseExpr resolve(final QNm name, final InputInfo info) throws QueryException { + public ParseExpr resolve(final QNm name, final InputInfo info) { // local variable final VarRef ref = resolveLocal(name, info); if(ref != null) return ref; - - // static variable - final byte[] uri = name.uri(); - - // accept variable reference... - // - if a variable uses the module or an imported URI, or - // - if it is specified in the main module - final QNm module = parser.sc.module; - final boolean hasImport = parser.moduleURIs.contains(uri); - if(module == null || eq(module.uri(), uri) || hasImport) - return parser.qc.vars.newRef(name, info, hasImport); - - throw parser.error(VARUNDEF_X, info, '$' + string(name.string())); + return parser.qc.vars.newRef(name, info, parser.moduleURIs); } /** diff --git a/basex-core/src/main/java/org/basex/query/value/item/QNm.java b/basex-core/src/main/java/org/basex/query/value/item/QNm.java index 4454358b31..1237e6d8fa 100644 --- a/basex-core/src/main/java/org/basex/query/value/item/QNm.java +++ b/basex-core/src/main/java/org/basex/query/value/item/QNm.java @@ -146,6 +146,15 @@ public byte[] uri() { return uri == null ? Token.EMPTY : uri; } + /** + * Returns the URI of the given QName, or an empty string if {@code null}. + * @param qnm name (can be {@code null}) + * @return URI + */ + public static byte[] uri(final QNm qnm) { + return qnm == null ? Token.EMPTY : qnm.uri(); + } + /** * Checks if the URI of this QName has been explicitly set. * @return result of check diff --git a/basex-core/src/main/java/org/basex/query/var/StaticVarRef.java b/basex-core/src/main/java/org/basex/query/var/StaticVarRef.java index 05a416bc6c..836f25184f 100644 --- a/basex-core/src/main/java/org/basex/query/var/StaticVarRef.java +++ b/basex-core/src/main/java/org/basex/query/var/StaticVarRef.java @@ -1,9 +1,6 @@ package org.basex.query.var; -import static org.basex.query.QueryError.*; - import org.basex.query.*; -import org.basex.query.ann.*; import org.basex.query.expr.*; import org.basex.query.util.*; import org.basex.query.value.*; @@ -20,21 +17,18 @@ */ final class StaticVarRef extends ParseExpr { /** Variable name. */ - private final QNm name; + public final QNm name; /** Referenced variable. */ private StaticVar var; - /** Indicates whether a module import for the variable name's URI was present. */ - final boolean hasImport; + /** * Constructor. * @param info input info (can be {@code null}) * @param name variable name - * @param hasImport indicates whether a module import for the variable name's URI was present */ - StaticVarRef(final InputInfo info, final QNm name, final boolean hasImport) { + StaticVarRef(final InputInfo info, final QNm name) { super(info, Types.ITEM_ZM); this.name = name; - this.hasImport = hasImport; } @Override @@ -75,7 +69,7 @@ public boolean accept(final ASTVisitor visitor) { @Override public Expr copy(final CompileContext cc, final IntObjectMap vm) { - final StaticVarRef ref = new StaticVarRef(info, name, hasImport); + final StaticVarRef ref = new StaticVarRef(info, name); ref.var = var; return copyType(ref); } @@ -104,11 +98,8 @@ public Expr inline(final InlineContext ic) { /** * Initializes this reference with the given variable. * @param vr variable - * @throws QueryException query exception */ - void init(final StaticVar vr) throws QueryException { - if(vr.anns.contains(Annotation.PRIVATE) && !sc().baseURI().eq(vr.sc.baseURI())) - throw VARPRIVATE_X.get(info, this); + void init(final StaticVar vr) { var = vr; } diff --git a/basex-core/src/main/java/org/basex/query/var/Variables.java b/basex-core/src/main/java/org/basex/query/var/Variables.java index 4d049034b7..d70a2d6fbc 100644 --- a/basex-core/src/main/java/org/basex/query/var/Variables.java +++ b/basex-core/src/main/java/org/basex/query/var/Variables.java @@ -5,26 +5,37 @@ import java.util.*; import org.basex.query.*; +import org.basex.query.ann.*; import org.basex.query.expr.*; import org.basex.query.util.hash.*; import org.basex.query.util.list.*; import org.basex.query.value.*; import org.basex.query.value.item.*; import org.basex.util.*; +import org.basex.util.hash.*; /** - * Container of global variables of a module. + * Container of global variables of a query. * * @author BaseX Team, BSD License * @author Leo Woerteler */ public final class Variables extends ExprInfo implements Iterable { - /** The variables. */ - private final QNmMap vars = new QNmMap<>(); + /** + * An unresolved variable reference. + * @param ref reference + * @param hasImport whether there was an import statement for the ref's URI + */ + private record UnresolvedRef(StaticVarRef ref, boolean hasImport) { } + /** All unresolved variable references. */ + private final ArrayList unresolvedRefs = new ArrayList<>(); + /** The variables by declaring module. */ + private final TokenObjectMap> varsByModule = new TokenObjectMap<>(); /** - * Declares a new static variable. + * Declares a new static variable in a given module. * @param var variable + * @param imports imported module URIs * @param expr bound expression, possibly {@code null} * @param anns annotations * @param external {@code external} flag @@ -33,10 +44,29 @@ public final class Variables extends ExprInfo implements Iterable { * @return static variable reference * @throws QueryException query exception */ - public StaticVar declare(final Var var, final Expr expr, final AnnList anns, - final boolean external, final VarScope vs, final String doc) throws QueryException { + public StaticVar declare(final Var var, final TokenSet imports, final Expr expr, + final AnnList anns, final boolean external, final VarScope vs, final String doc) + throws QueryException { + + final byte[] modUri = QNm.uri(var.info.sc().module); + final byte[] varUri = var.name.uri(); + if(!Token.eq(modUri, varUri)) { + if(modUri != Token.EMPTY && !anns.contains(Annotation.PRIVATE)) { + throw MODULENS_X.get(var.info, var); + } + if(imports.contains(varUri)) { + final StaticVar sv = get(var.name, varUri); + if(sv != null && !sv.anns.contains(Annotation.PRIVATE)) { + // variable already declared in imported module + throw VARDUPL_X.get(var.info, var.name.string()); + } + } + } + final QNmMap vars = varsByModule.computeIfAbsent(modUri, QNmMap::new); + if(vars.contains(var.name)) throw VARDUPL_X.get(var.info, var.name.string()); + final StaticVar sv = new StaticVar(var, expr, anns, external, vs, doc); - varEntry(var.name).setVar(sv); + vars.put(var.name, sv); return sv; } @@ -45,30 +75,37 @@ public StaticVar declare(final Var var, final Expr expr, final AnnList anns, * @throws QueryException query exception */ public void checkUp() throws QueryException { - for(final VarEntry ve : vars.values()) ve.var.checkUp(); + for(final StaticVar var : this) var.checkUp(); } /** - * Checks if all variables were declared and are visible to all their references. + * Resolves all references and checks for existing and visible declarations. * @throws QueryException query exception */ - public void check() throws QueryException { - for(final VarEntry ve : vars.values()) { - final StaticVar var = ve.var; + public void resolve() throws QueryException { + for(final UnresolvedRef ur : unresolvedRefs) { + final StaticVarRef ref = ur.ref; + + // try to resolve the reference within the local module + final byte[] modUri = QNm.uri(ref.sc().module); + StaticVar var = get(ref.name, modUri); + if(var == null) { - final StaticVarRef ref = ve.refs.get(0); - throw VARUNDEF_X.get(ref.info(), ref); - } - final QNm varMod = var.info.sc().module; - final byte[] varModUri = varMod == null ? Token.EMPTY : varMod.uri(); - for(final StaticVarRef ref : ve.refs) { - if(!ref.hasImport) { - final QNm refMod = ref.info().sc().module; - final byte[] refModUri = refMod == null ? Token.EMPTY : refMod.uri(); - if(!Token.eq(varModUri, refModUri)) throw INVISIBLEVAR_X.get(ref.info(), var.name); + // try to resolve the reference from a module + final byte[] refUri = ref.name.uri(); + var = get(ref.name, refUri); + if(var == null) throw VARUNDEF_X.get(ref.info(), ref); + if(!Token.eq(modUri, refUri) && !ur.hasImport) { + throw INVISIBLEVAR_X.get(ref.info(), ref.name); } } + + if(var.anns.contains(Annotation.PRIVATE) && !Token.eq(modUri, QNm.uri(var.sc.module))) { + throw VARPRIVATE_X.get(ref.info(), ref); + } + ref.init(var); } + unresolvedRefs.clear(); } /** @@ -77,38 +114,32 @@ public void check() throws QueryException { * @throws QueryException query exception */ public void compileAll(final CompileContext cc) throws QueryException { - for(final StaticVar var : this) { - var.compile(cc); - } + for(final StaticVar var : this) var.compile(cc); } /** * Returns a new reference to the (possibly not yet declared) variable with the given name. * @param name variable name - * @param info input info (can be {@code null}) - * @param hasImport indicates whether a module import for the variable name's URI was present + * @param info input info + * @param imports URIs of imported modules * @return reference - * @throws QueryException if the variable is not visible */ - public StaticVarRef newRef(final QNm name, final InputInfo info, final boolean hasImport) - throws QueryException { - final StaticVarRef ref = new StaticVarRef(info, name, hasImport); - varEntry(name).addRef(ref); + public StaticVarRef newRef(final QNm name, final InputInfo info, final TokenSet imports) { + if(info == null) throw Util.notExpected(); + final StaticVarRef ref = new StaticVarRef(info, name); + unresolvedRefs.add(new UnresolvedRef(ref, imports.contains(name.uri()))); return ref; } /** - * Returns a variable entry for the specified QName. + * Returns the variable for the specified QName and module, or {@code null} if it does not exist. * @param name QName - * @return variable entry + * @param module module URI + * @return variable entry, or {@code null} */ - private VarEntry varEntry(final QNm name) { - VarEntry entry = vars.get(name); - if(entry == null) { - entry = new VarEntry(); - vars.put(name, entry); - } - return entry; + private StaticVar get(final QNm name, final byte[] module) { + final QNmMap vars = varsByModule.get(module); + return vars == null ? null : vars.get(name); } /** @@ -123,73 +154,51 @@ public void bindExternal(final QueryContext qc, final QNmMap bindings, fi for(final QNm qnm : bindings) { if(qnm != QNm.EMPTY) { - final VarEntry ve = vars.get(qnm); - if(ve != null) ve.var.bind(bindings.get(qnm), qc, cast); + final Value val = bindings.get(qnm); + for(final QNmMap vars : varsByModule.values()) { + final StaticVar var = vars.get(qnm); + if(var != null) var.bind(val, qc, cast); + } } } } @Override public Iterator iterator() { - final Iterator qnames = vars.iterator(); return new Iterator<>() { + /** Iterator over modules. */ + private final Iterator> modules = varsByModule.values().iterator(); + /** Iterator over StaticVar objects of the current module. */ + private Iterator vars = Collections.emptyIterator(); + @Override public boolean hasNext() { - return qnames.hasNext(); + while(!vars.hasNext()) { + if(!modules.hasNext()) return false; + vars = modules.next().values().iterator(); + } + return true; } @Override public StaticVar next() { - return vars.get(qnames.next()).var; - } - - @Override - public void remove() { - throw Util.notExpected(); + if(!vars.hasNext()) throw new NoSuchElementException(); + return vars.next(); } }; } @Override public void toXml(final QueryPlan plan) { - if(vars.isEmpty()) return; + if(varsByModule.isEmpty()) return; - final ArrayList list = new ArrayList<>(vars.size()); - for(final VarEntry ve : vars.values()) list.add(ve.var); + final ArrayList list = new ArrayList<>(); + for(final StaticVar var : this) list.add(var); plan.add(plan.create(this), list.toArray()); } @Override public void toString(final QueryString qs) { - for(final VarEntry ve : vars.values()) qs.token(ve.var); - } - - /** Entry for static variables and their references. */ - private static final class VarEntry { - /** The static variable. */ - StaticVar var; - /** Variable references. */ - final ArrayList refs = new ArrayList<>(1); - - /** - * Sets the variable for existing references. - * @param vr variable to set - * @throws QueryException if the variable was already declared - */ - void setVar(final StaticVar vr) throws QueryException { - if(var != null) throw VARDUPL_X.get(vr.info, var.name.string()); - var = vr; - for(final StaticVarRef ref : refs) ref.init(var); - } - - /** - * Adds a reference to this variable. - * @param ref reference to add - * @throws QueryException query exception - */ - void addRef(final StaticVarRef ref) throws QueryException { - refs.add(ref); - if(var != null) ref.init(var); - } + for(final StaticVar var : this) qs.token(var); } } diff --git a/basex-core/src/test/java/org/basex/query/ModuleTest.java b/basex-core/src/test/java/org/basex/query/ModuleTest.java index fd03e5bab9..235540e328 100644 --- a/basex-core/src/test/java/org/basex/query/ModuleTest.java +++ b/basex-core/src/test/java/org/basex/query/ModuleTest.java @@ -145,6 +145,39 @@ public final class ModuleTest extends SandboxTest { + "', '" + o.path() + "') })", QueryError.MODULE_FOUND_OTHER_X_X); } + /** Tests variable visibility. */ + @Test public void variableVisibility() { + final IOFile sandbox = sandbox(); + final IOFile m = new IOFile(sandbox, "m.xqm"); + write(m, "module namespace m = 'm';\n" + + "declare %private variable $m:x := 'module';\n" + + "declare function m:f() {$m:x};"); + final IOFile n1 = new IOFile(sandbox, "n1.xqm"); + write(n1, "module namespace n = 'n';\n" + + "declare function n:f() {$x};"); + final IOFile n2 = new IOFile(sandbox, "n2.xqm"); + write(n2, "module namespace n = 'n';\n" + + "declare %private variable $x := 42;"); + final IOFile o = new IOFile(sandbox, "o.xqm"); + write(o, "module namespace o = 'o';\n" + + "import module 'o' at '" + o.path() + "';\n" + + "declare function o:f() {$o:x};"); + + // private variable does not clash with the same name in another module + query("import module namespace m = 'm' at '" + m.path() + "';\n" + + "declare variable $m:x := 'main';\n" + + "m:f(), $m:x", "module\nmain"); + + // private variable is visible throughout module, even in different file + query("import module namespace n = 'n' at '" + n1.path() + "', '" + n2.path() + "';\n" + + "n:f()", 42); + + // variable in main module is not visible in imported module + error("import module namespace o = 'o' at '" + o.path() + "';\n" + + "declare variable $o:x := 42;\n" + + "o:f()", QueryError.VARUNDEF_X); + } + /** Tests rejection of functions and variables, when their modules are not explicitly imported. */ @Test public void gh2048() { final IOFile sandbox = sandbox();