Skip to content

Commit

Permalink
Merge pull request #43421 from MaryamZi/fix-xml-member-access-type
Browse files Browse the repository at this point in the history
Fix XML member access type and allow XML langlib calls with unions
  • Loading branch information
gimantha authored Oct 8, 2024
2 parents f392e37 + ebe9bc4 commit b67b420
Show file tree
Hide file tree
Showing 13 changed files with 563 additions and 32 deletions.
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);
}

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
50 changes: 50 additions & 0 deletions langlib/langlib-test/src/test/resources/test-src/xmllib_test.bal
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

0 comments on commit b67b420

Please sign in to comment.