Skip to content
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

Fix XML member access type and allow XML langlib calls with unions #43421

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,6 @@ public enum DiagnosticErrorCode implements DiagnosticCode {

CONTINUE_NOT_ALLOWED("BCE3992", "continue.not.allowed"),
BREAK_NOT_ALLOWED("BCE3993", "break.not.allowed"),
XML_FUNCTION_DOES_NOT_SUPPORT_ARGUMENT_TYPE("BCE3995", "xml.function.does.not.support.argument.type"),

INTERSECTION_NOT_ALLOWED_WITH_TYPE("BCE3996", "intersection.not.allowed.with.type"),
ASYNC_SEND_NOT_YET_SUPPORTED_AS_EXPRESSION("BCE3997", "async.send.action.not.yet.supported.as.expression"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6742,7 +6742,7 @@ private void visitArgs(BLangInvocation invocation) {
reorderArguments(invocation);

rewriteExprs(invocation.requiredArgs);
fixStreamTypeCastsInInvocationParams(invocation);
fixStreamAndXmlTypeCastsInInvocationParams(invocation);
fixNonRestArgTypeCastInTypeParamInvocation(invocation);

rewriteExprs(invocation.restArgs);
Expand Down Expand Up @@ -7188,13 +7188,14 @@ private BLangExpression fixTypeCastInTypeParamInvocation(BLangInvocation iExpr,
return types.addConversionExprIfRequired(genIExpr, originalInvType);
}

private void fixStreamTypeCastsInInvocationParams(BLangInvocation iExpr) {
private void fixStreamAndXmlTypeCastsInInvocationParams(BLangInvocation iExpr) {
List<BLangExpression> requiredArgs = iExpr.requiredArgs;
List<BVarSymbol> params = ((BInvokableSymbol) iExpr.symbol).params;
if (!params.isEmpty()) {
for (int i = 0; i < requiredArgs.size(); i++) {
BVarSymbol param = params.get(i);
if (Types.getImpliedType(param.type).tag == TypeTags.STREAM) {
int tag = Types.getImpliedType(param.type).tag;
if (tag == TypeTags.STREAM || tag == TypeTags.XML) {
requiredArgs.set(i, types.addConversionExprIfRequired(requiredArgs.get(i), param.type));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8910,10 +8910,10 @@ private BType checkIndexAccessExpr(BLangIndexBasedAccess indexBasedAccessExpr, A
if (type == symTable.semanticError) {
return type;
}
// Note: out of range member access returns empty xml value unlike lists
// hence, this needs to be set to xml type
indexBasedAccessExpr.originalType = varRefType;
actualType = varRefType;

BType xmlMemberAccessType = getXmlMemberAccessType(varRefType);
indexBasedAccessExpr.originalType = xmlMemberAccessType;
actualType = xmlMemberAccessType;
} else if (varRefType.tag == TypeTags.TABLE) {
if (indexBasedAccessExpr.isLValue) {
dlog.error(indexBasedAccessExpr.pos, DiagnosticErrorCode.CANNOT_UPDATE_TABLE_USING_MEMBER_ACCESS,
Expand Down Expand Up @@ -8965,6 +8965,34 @@ private BType checkIndexAccessExpr(BLangIndexBasedAccess indexBasedAccessExpr, A
return actualType;
}

private BType getXmlMemberAccessType(BType varRefType) {
BType xmlMemberAccessType;

if (varRefType.tag == TypeTags.UNION) {
LinkedHashSet<BType> memberTypes = ((BUnionType) varRefType).getMemberTypes();
LinkedHashSet<BType> effectiveMemberTypes = new LinkedHashSet<>(memberTypes.size());
for (BType memberType : memberTypes) {
memberType = Types.getImpliedType(memberType);
if (memberType == symTable.xmlNeverType) {
effectiveMemberTypes.add(symTable.xmlNeverType);
continue;
}
effectiveMemberTypes.add(getXMLConstituents(memberType));
}
xmlMemberAccessType = effectiveMemberTypes.size() == 1 ? effectiveMemberTypes.iterator().next() :
BUnionType.create(null, effectiveMemberTypes);
} else {
xmlMemberAccessType = getXMLConstituents(varRefType);
}

if (types.isAssignable(xmlMemberAccessType, symTable.neverType)) {
return symTable.xmlNeverType;
} else if (types.isAssignable(symTable.xmlNeverType, xmlMemberAccessType)) {
return xmlMemberAccessType;
}
return BUnionType.create(null, xmlMemberAccessType, symTable.xmlNeverType);
gimantha marked this conversation as resolved.
Show resolved Hide resolved
}

private Long getConstIndex(BLangExpression indexExpr) {
return switch (indexExpr.getKind()) {
case GROUP_EXPR -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,7 @@ private void visitType(BLangExpression expr, Location loc, BType expType, BType
// Bound type is a structure. Visit recursively to find bound type.
switch (expType.tag) {
case TypeTags.XML:
if (!TypeTags.isXMLTypeTag(Types.getImpliedType(actualType).tag)) {
if (Types.getImpliedType(actualType).tag == TypeTags.UNION) {
dlog.error(loc, DiagnosticErrorCode.XML_FUNCTION_DOES_NOT_SUPPORT_ARGUMENT_TYPE, actualType);
}
if (!types.isAssignable(actualType, symTable.xmlType)) {
return;
}
switch (actualType.tag) {
Expand Down Expand Up @@ -653,6 +650,7 @@ private void findTypeParamInUnion(Location loc, BType expType, BUnionType actual
if (TypeTags.isXMLTypeTag(referredType.tag)) {
if (referredType.tag == TypeTags.XML) {
members.add(((BXMLType) referredType).constraint);
continue;
}
members.add(type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -721,10 +721,6 @@ error.invalid.namespace.declaration=\
error.cannot.update.xml.sequence=\
cannot update an xml sequence

error.xml.function.does.not.support.argument.type=\
xml langlib functions does not support union types as their arguments


error.invalid.xml.ns.interpolation=\
xml namespaces cannot be interpolated

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ public void testXmlFilterValueAndErrorWithNonElementSingletonValues() {
BRunUtil.invoke(compileResult, "testXmlFilterValueAndErrorWithNonElementSingletonValues");
}

@Test
public void testLangLibCallsWithUnions() {
BRunUtil.invoke(compileResult, "testLangLibCallsWithUnions");
}

@Test
public void testNegativeCases() {
negativeResult = BCompileUtil.compile("test-src/xmllib_test_negative.bal");
Expand All @@ -392,6 +397,28 @@ public void testNegativeCases() {
97, 62);
validateError(negativeResult, i++, "incompatible types: expected 'string', found 'xml:Element'",
98, 41);
validateError(negativeResult, i++, "incompatible types: expected 'xml', " +
"found '(xml<xml:Comment>|xml<xml:Element>|int)'", 103, 13);
validateError(negativeResult, i++, "incompatible types: expected 'xml', " +
"found '(xml<xml:Comment>|xml<xml:Element>|int)'", 104, 20);
validateError(negativeResult, i++, "incompatible types: expected 'object { " +
"public isolated function next () returns (" +
"record {| xml:Comment value; |}?); }', found 'object { " +
"public isolated function next () returns (" +
"record {| (xml:Comment|xml:ProcessingInstruction) value; |}?); }'", 111, 11);
validateError(negativeResult, i++, "incompatible types: expected 'xml:Comment', " +
"found '(xml:Comment|xml:ProcessingInstruction)'", 113, 21);
validateError(negativeResult, i++, "incompatible types: expected 'xml:ProcessingInstruction', " +
"found '(xml:Comment|xml:ProcessingInstruction)'", 114, 35);
validateError(negativeResult, i++, "incompatible types: expected 'xml:Comment', " +
"found '(xml:Comment|xml:Element)'", 117, 21);
validateError(negativeResult, i++, "incompatible types: expected 'xml:Element', " +
"found '(xml:Comment|xml:Element)'", 118, 21);
validateError(negativeResult, i++, "incompatible types: expected 'xml<never>', " +
"found '(xml:Element|never)'", 121, 20);
validateError(negativeResult, i++, "incompatible types: expected 'xml:Element', found 'xml'", 124, 21);
validateError(negativeResult, i++, "incompatible types: expected 'xml:ProcessingInstruction', " +
"found 'xml:Comment'", 127, 35);
assertEquals(negativeResult.getErrorCount(), i);
}

Expand Down Expand Up @@ -449,8 +476,8 @@ public void testNegativeConstraint() {
25, 28);
validateError(constraintNegative, i++, "incompatible types: expected 'xml<xml:ProcessingInstruction>'," +
" found 'xml:Element'", 26, 42);
validateError(constraintNegative, i++, "incompatible types: expected 'xml:Comment', found 'xml<xml:Element>'",
29, 26);
validateError(constraintNegative, i++, "incompatible types: expected 'xml:Comment', " +
"found '(xml:Element|xml<never>)'", 29, 26);
validateError(constraintNegative, i++, "incompatible types: expected 'xml<xml:Element>', found 'xml:Comment'",
32, 41);
validateError(constraintNegative, i++, "incompatible types: expected 'xml<xml:Comment>'," +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,56 @@ function testXmlFilterValueAndErrorWithNonElementSingletonValues() {
"incompatible types: 'lang.xml:Text' cannot be cast to 'lang.xml:Element'");
}

function testLangLibCallsWithUnions() {
xml<xml:Comment>|xml<xml:Element> s1 = <xml<xml:Element>> xml `<foo/><bar>val</bar>`;
int length = s1.length();
test:assertValueEqual(length, 2);
length = xml:length(s1);
test:assertValueEqual(length, 2);

xml<xml:Comment>|xml<xml:ProcessingInstruction> s2 = <xml<xml:Comment>> xml `<!--foo--><!--bar-->`;
object {
public isolated function next() returns record {|
xml:Comment|xml:ProcessingInstruction value;
|}?;
} iterator = s2.iterator();
test:assertValueEqual(iterator.next(), {value: xml `<!--foo-->`});
test:assertValueEqual(iterator.next(), {value: xml `<!--bar-->`});
test:assertValueEqual(iterator.next(), ());

xml:Comment|xml:Element get = s1.get(1);
test:assertValueEqual(get, xml `<bar>val</bar>`);
test:assertValueEqual(xml:get(s1, 1), xml `<bar>val</bar>`);

xml:Comment|xml:Element get2 = get.get(0);
test:assertValueEqual(get2, xml `<bar>val</bar>`);

get2 = xml:get(get, 0);
test:assertValueEqual(get2, xml `<bar>val</bar>`);

xml:Element|xml<never> v1 = xml `<baz/>`;
xml:Element get3 = v1.get(0);
test:assertValueEqual(get3, xml `<baz/>`);

var fn = function () {
xml:Element|xml<never> v = xml ``;
xml:Element _ = v.get(1);
panic error("expected the get call to panic");
};
error? fnRes = trap fn();
test:assertTrue(fnRes is error);
error err = <error> fnRes;
test:assertValueEqual(err.message(), "xml sequence index out of range. Length: '1' requested: '1'");

xml:Element|xml:Element v2 = xml `<books><book>Hamlet</book><book>Macbeth</book></books>`;
xml children = v2.getChildren();
test:assertValueEqual(children, xml `<book>Hamlet</book><book>Macbeth</book>`);

xml<xml:Comment>|xml<xml:Comment> s3 = <xml<xml:Comment>> xml `<!--foo--><!--bar-->`;
xml:Comment get4 = s3.get(0);
test:assertValueEqual(get4, xml `<!--foo-->`);
}

type Error error<record {string message;}>;

function assertError(any|error value, string errorMessage, string expDetailMessage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,32 @@ function testCreateElement() {
xml:Element r3 = 'xml:createElement("elem", attributes3, "<ele>hello</ele>");
xml:Element r4 = 'xml:createElement(xml `<ele/>`, attributes3, children1);
}

function testLangLibCallsWithUnionsNegative() {
xml<xml:Comment>|xml<xml:Element>|int s1 = <xml<xml:Element>> xml `<foo/><bar>val</bar>`;
int _ = s1.length();
_ = xml:length(s1);

xml<xml:Comment>|xml<xml:ProcessingInstruction> s2 = <xml<xml:Comment>> xml `<!--foo--><!--bar-->`;
object {
public isolated function next() returns record {|
xml:Comment value;
|}?;
} _ = s2.iterator();

xml:Comment _ = s2.get(1);
xml:ProcessingInstruction _ = xml:get(s2, 1);

xml:Comment|xml:Element v = xml `<foo/>`;
xml:Comment _ = v.get(0);
xml:Element _ = xml:get(v, 0);

xml:Element|xml<never> v1 = xml `<baz/>`;
xml<never> _ = v1.get(0);

xml:Element|xml:Element v2 = xml `<books><book>Hamlet</book><book>Macbeth</book></books>`;
xml:Element _ = v2.getChildren();

xml<xml:Comment>|xml<xml:Comment> s3 = <xml<xml:Comment>> xml `<!--foo--><!--bar-->`;
xml:ProcessingInstruction _ = s3.get(0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you 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
*
* http://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.ballerinalang.test.expressions.access;

import org.ballerinalang.test.BCompileUtil;
import org.ballerinalang.test.BRunUtil;
import org.ballerinalang.test.CompileResult;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.ballerinalang.test.BAssertUtil.validateError;

/**
* Test cases for XML member access.
*
* @since 2201.11.0
*/
public class XmlMemberAccessTest {

private CompileResult result;

@BeforeClass
public void setup() {
result = BCompileUtil.compile("test-src/expressions/access/xml_member_access.bal");
}

@Test
public void testNegativeCases() {
CompileResult negativeResult = BCompileUtil.compile(
"test-src/expressions/access/xml_member_access_negative.bal");
int i = 0;
validateError(negativeResult, i++, "incompatible types: expected 'xml:Element', " +
"found '(xml:Element|xml:Comment|xml:ProcessingInstruction|xml:Text)'", 20, 21);
validateError(negativeResult, i++, "incompatible types: expected " +
"'(xml:Element|xml:Comment|xml:ProcessingInstruction)', found " +
"'(xml:Element|xml:Comment|xml:ProcessingInstruction|xml:Text)'", 21, 59);
validateError(negativeResult, i++, "incompatible types: expected " +
"'(xml:Comment|xml:ProcessingInstruction|xml:Text)', found " +
"'(xml:Element|xml:Comment|xml:ProcessingInstruction|xml:Text)'", 22, 56);
validateError(negativeResult, i++, "incompatible types: expected 'xml:Element', " +
"found '(xml:Element|xml<never>)'", 27, 21);
validateError(negativeResult, i++, "incompatible types: expected 'xml<never>', " +
"found '(xml:Element|xml<never>)'", 28, 20);
validateError(negativeResult, i++, "incompatible types: expected 'xml:Comment', " +
"found '(xml:Comment|xml<never>)'", 33, 21);
validateError(negativeResult, i++, "incompatible types: expected 'xml<never>', " +
"found '(xml:Comment|xml<never>)'", 34, 20);
validateError(negativeResult, i++, "incompatible types: expected 'xml<never>', found 'xml:Text'", 39, 20);
validateError(negativeResult, i++, "incompatible types: expected 'xml:ProcessingInstruction', " +
"found '(xml:ProcessingInstruction|xml<never>)'", 44, 35);
validateError(negativeResult, i++, "incompatible types: expected 'xml<never>', " +
"found '(xml:ProcessingInstruction|xml<never>)'", 45, 20);
validateError(negativeResult, i++, "incompatible types: expected 'xml:Element', " +
"found '(xml:Element|xml<never>)'", 50, 21);
validateError(negativeResult, i++, "incompatible types: expected 'xml<never>', found 'xml:Text'", 53, 20);
validateError(negativeResult, i++, "incompatible types: expected 'xml:Comment', " +
"found '(xml:Comment|xml<never>)'", 56, 21);
validateError(negativeResult, i++, "incompatible types: expected 'xml:ProcessingInstruction', " +
"found '(xml:ProcessingInstruction|xml<never>)'", 59, 35);
validateError(negativeResult, i++, "incompatible types: expected '(xml:Element|xml:Comment)', " +
"found '(xml:Element|xml:Comment|xml<never>)'", 64, 33);
validateError(negativeResult, i++, "incompatible types: expected '(xml:Element|xml<never>)', " +
"found '(xml:Element|xml:Comment|xml<never>)'", 65, 32);
validateError(negativeResult, i++, "incompatible types: expected '(xml:Comment|xml<never>)', " +
"found '(xml:Comment|xml:ProcessingInstruction|never|xml<never>)'", 68, 32);
validateError(negativeResult, i++, "incompatible types: expected '(xml:Element|xml:Comment)', " +
"found '(xml:Element|xml:Comment|xml<never>)'", 73, 33);
validateError(negativeResult, i++, "incompatible types: expected '(xml:Comment|xml<never>)', " +
"found '(xml:Element|xml:Comment|xml<never>)'", 74, 32);
validateError(negativeResult, i++, "incompatible types: expected '(xml:Comment|xml<never>)', " +
"found '(xml:Comment|xml:ProcessingInstruction|never|xml<never>)'", 77, 32);
validateError(negativeResult, i++, "incompatible types: expected 'xml:Element', found 'xml<never>'", 82, 21);
Assert.assertEquals(negativeResult.getErrorCount(), i);
}

@Test(dataProvider = "xmlMemberAccessFunctions")
public void testXmlMemberAccess(String function) {
BRunUtil.invoke(result, function);
}

@DataProvider(name = "xmlMemberAccessFunctions")
public Object[][] xmlMemberAccessFunctions() {
return new Object[][] {
{ "testXmlMemberAccessOnXml" },
{ "testXmlMemberAccessOnXmlElementSequences" },
{ "testXmlMemberAccessOnXmlCommentSequences" },
{ "testXmlMemberAccessOnXmlTextSequences" },
{ "testXmlMemberAccessOnXmlProcessingInstructionSequences" },
{ "testXmlMemberAccessOnXmlSingletons" },
{ "testXmlMemberAccessOnXmlUnions" },
{ "testXmlMemberAccessOnXmlUnionSequences" },
{ "testXmlMemberAccessOnEmptySequenceType" }
};
}
}
Loading