Skip to content

Commit

Permalink
WidgetMap: support generics in objectTypes
Browse files Browse the repository at this point in the history
Parse typeArguments while converting an IdeaConstantValue to an
ES6Class. In addition, unwrap JSParenthesizedExpression and (for
IJ >= 2023.2) TypeScriptExpressionWithTypeArguments.
Fix NPE while resolving the export target.

377491
  • Loading branch information
fschinkel committed Apr 22, 2024
1 parent 31c6a58 commit b200273
Show file tree
Hide file tree
Showing 16 changed files with 341 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -10,17 +10,21 @@
package org.eclipse.scout.sdk.core.s.widgetmap;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.HashSet;
import java.util.List;

import org.eclipse.scout.sdk.core.typescript.TypeScriptTypes;
import org.eclipse.scout.sdk.core.typescript.model.api.IDataType;
import org.eclipse.scout.sdk.core.typescript.model.api.IES6Class;
import org.eclipse.scout.sdk.core.typescript.model.api.IFunction;
import org.eclipse.scout.sdk.core.typescript.model.api.INodeModule;
import org.eclipse.scout.sdk.core.typescript.testing.ExtendWithNodeModules;
import org.eclipse.scout.sdk.core.typescript.testing.spi.TestingNodeElementFactorySpi;
import org.junit.jupiter.api.Test;

public class IdObjectTypeMapTest {
Expand Down Expand Up @@ -102,9 +106,18 @@ public void testWidgetMapWithTable(INodeModule module) {

var groupBoxObjectType = createObjectType("GroupBox", module);
var stringFieldObjectType = createObjectType("StringField", module);
var smartFieldObjectType = createObjectType("SmartField", module);
var smartFieldNumberObjectType = createObjectType(module.nodeElementFactory()
.createClassWithTypeArguments(
smartFieldObjectType.es6Class(),
List.of(TestingNodeElementFactorySpi.newDataType(TypeScriptTypes._number))));
var tableFieldObjectType = createObjectType("TableField", module);
var menuObjectType = createObjectType("Menu", module);
var columnObjectType = createObjectType("Column", module);
var columnObjectObjectType = createObjectType(module.nodeElementFactory()
.createClassWithTypeArguments(
columnObjectType.es6Class(),
List.of(TestingNodeElementFactorySpi.newDataType(TypeScriptTypes._object))));
var numberColumnObjectType = createObjectType("NumberColumn", module);

var someFormModel = module.export("SomeFormWithTableFieldModel")
Expand All @@ -125,6 +138,10 @@ public void testWidgetMapWithTable(INodeModule module) {
// objectType: StringField
// },
// {
// id: 'TypeField',
// objectType: SmartField<number>
// },
// {
// id: 'SomeTableField',
// objectType: TableField,
// table: {
Expand Down Expand Up @@ -160,8 +177,8 @@ public void testWidgetMapWithTable(INodeModule module) {
// objectType: FancyTable,
// columns: [
// {
// id: 'TextColumn',
// objectType: Column
// id: 'ObjectColumn',
// objectType: Column<object>
// }
// ]
// }
Expand All @@ -172,8 +189,8 @@ public void testWidgetMapWithTable(INodeModule module) {

assertEquals("SomeFormWithTableFieldWidgetMap", widgetMap.name());

assertEquals(6, widgetMap.elements().size());
assertEquals(List.of("MainBox", "TitleField", "SomeTableField", "SomeTable", "FancyTableField", "Table"), widgetMap.elements().keySet().stream().toList());
assertEquals(7, widgetMap.elements().size());
assertEquals(List.of("MainBox", "TitleField", "TypeField", "SomeTableField", "SomeTable", "FancyTableField", "Table"), widgetMap.elements().keySet().stream().toList());

assertEquals(1, widgetMap.idObjectTypeMapReferences().size());

Expand All @@ -185,6 +202,10 @@ public void testWidgetMapWithTable(INodeModule module) {
assertNotNull(titleField);
assertObjectType(stringFieldObjectType, titleField.objectType());

var typeField = widgetMap.elements().get("TypeField");
assertNotNull(typeField);
assertObjectType(smartFieldNumberObjectType, typeField.objectType());

var someTableField = widgetMap.elements().get("SomeTableField");
assertNotNull(someTableField);
assertObjectType(tableFieldObjectType, someTableField.objectType());
Expand Down Expand Up @@ -240,14 +261,14 @@ public void testWidgetMapWithTable(INodeModule module) {
assertEquals("FancyTableFieldTableColumnMap", fancyTableFieldTableColumnMap.name());

assertEquals(1, fancyTableFieldTableColumnMap.elements().size());
assertEquals(List.of("TextColumn"), fancyTableFieldTableColumnMap.elements().keySet().stream().toList());
assertEquals(List.of("ObjectColumn"), fancyTableFieldTableColumnMap.elements().keySet().stream().toList());

assertEquals(1, fancyTableFieldTableColumnMap.idObjectTypeMapReferences().size());
assertIdObjectTypeMapReference(IdObjectTypeMapReference.create(findClassByObjectType("FancyTableColumnMap", module)).orElseThrow(), fancyTableFieldTableColumnMap.idObjectTypeMapReferences().stream().findFirst().orElseThrow());

var textColumn = fancyTableFieldTableColumnMap.elements().get("TextColumn");
assertNotNull(textColumn);
assertObjectType(columnObjectType, textColumn.objectType());
var objectColumn = fancyTableFieldTableColumnMap.elements().get("ObjectColumn");
assertNotNull(objectColumn);
assertObjectType(columnObjectObjectType, objectColumn.objectType());
}

@Test
Expand Down Expand Up @@ -555,10 +576,32 @@ private static void assertSomePageModel(INodeModule module, String expectedDetai

private static void assertObjectType(ObjectType expected, ObjectType actual) {
assertNotNull(actual);
assertSame(expected.es6Class(), actual.es6Class());
assertSame(expected.es6Class().withoutTypeArguments(), actual.es6Class().withoutTypeArguments());
assertTypeArguments(expected.es6Class().typeArguments().toList(), actual.es6Class().typeArguments().toList());
assertEquals(expected.newClassName(), actual.newClassName());
}

private static void assertES6Class(IES6Class expected, IES6Class actual) {
assertSame(expected.withoutTypeArguments(), actual.withoutTypeArguments());
assertTypeArguments(expected.typeArguments().toList(), actual.typeArguments().toList());
}

private static void assertTypeArguments(List<IDataType> expected, List<IDataType> actual) {
assertNotNull(actual);
assertEquals(expected.size(), actual.size());
for (var i = 0; i < expected.size(); i++) {
var e = expected.get(i);
var a = actual.get(i);
if (e instanceof IES6Class expectedES6Class) {
var actualES6Class = assertInstanceOf(IES6Class.class, a);
assertES6Class(expectedES6Class, actualES6Class);
continue;
}
assertEquals(e.isPrimitive(), a.isPrimitive());
assertEquals(e.name(), a.name());
}
}

private static void assertIdObjectTypeMapReference(IdObjectTypeMapReference expected, IdObjectTypeMapReference actual) {
assertNotNull(actual);
expected.es6Class().ifPresent(es6Class -> assertSame(es6Class, actual.es6Class().orElseThrow()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand Down Expand Up @@ -108,6 +108,10 @@ public void testWidgetMapWithTable(INodeModule module) {
// objectType: StringField
// },
// {
// id: 'TypeField',
// objectType: SmartField<number>
// },
// {
// id: 'SomeTableField',
// objectType: TableField,
// table: {
Expand Down Expand Up @@ -143,8 +147,8 @@ public void testWidgetMapWithTable(INodeModule module) {
// objectType: FancyTable,
// columns: [
// {
// id: 'TextColumn',
// objectType: Column
// id: 'ObjectColumn',
// objectType: Column<object>
// }
// ]
// }
Expand All @@ -161,6 +165,7 @@ public void testWidgetMapWithTable(INodeModule module) {
"export type SomeFormWithTableFieldWidgetMap = {" +
"'MainBox': GroupBox;" +
"'TitleField': StringField;" +
"'TypeField': SmartField<number>;" +
"'SomeTableField': TableField;" +
"'SomeTable': SomeTable;" +
"'FancyTableField': TableField;" +
Expand All @@ -182,14 +187,14 @@ public void testWidgetMapWithTable(INodeModule module) {
"declare columnMap: FancyTableFieldTableColumnMap;" +
"}",
"export type FancyTableFieldTableColumnMap = {" +
"'TextColumn': Column;" +
"'ObjectColumn': Column<object>;" +
"} & FancyTableColumnMap;"

), operation.classSources());

var imports = operation.importsForModel().stream().map(ES6ImportDescriptor::nameForSource).toList();
assertEquals(List.of("GroupBox", "StringField", "TableField", "SomeTable", "FancyTableFieldTable", "SomeTableWidgetMap", "Table", "SomeTableColumnMap", "Menu", "NumberColumn", "Column", "FancyTable", "FancyTableFieldTableColumnMap",
"FancyTableColumnMap"), imports);
assertEquals(List.of("GroupBox", "StringField", "SmartField", "TableField", "SomeTable", "FancyTableFieldTable", "SomeTableWidgetMap", "Table", "SomeTableColumnMap", "Menu", "NumberColumn", "Column", "FancyTable",
"FancyTableFieldTableColumnMap", "FancyTableColumnMap"), imports);

var declarationSources = operation.declarationSources();
assertEquals(1, declarationSources.size());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright (c) 2010, 2023 BSI Business Systems Integration AG
~ Copyright (c) 2010, 2024 BSI Business Systems Integration AG
~
~ This program and the accompanying materials are made
~ available under the terms of the Eclipse Public License 2.0
Expand Down Expand Up @@ -62,6 +62,29 @@
</objectLiteral>
</value>
</constantValue>
<constantValue>
<type>ObjectLiteral</type>
<value>
<objectLiteral>
<property name="id">
<constantValue>
<type>String</type>
<value>TypeField</value>
</constantValue>
</property>
<property name="objectType">
<constantValue>
<type>ES6Class</type>
<value>
<ref name="SmartField" module="@eclipse-scout/core">
<typeArgument>number</typeArgument>
</ref>
</value>
</constantValue>
</property>
</objectLiteral>
</value>
</constantValue>
<constantValue>
<type>ObjectLiteral</type>
<value>
Expand Down Expand Up @@ -252,14 +275,16 @@
<property name="id">
<constantValue>
<type>String</type>
<value>TextColumn</value>
<value>ObjectColumn</value>
</constantValue>
</property>
<property name="objectType">
<constantValue>
<type>ES6Class</type>
<value>
<ref name="Column" module="@eclipse-scout/core"></ref>
<ref name="Column" module="@eclipse-scout/core">
<typeArgument>object</typeArgument>
</ref>
</value>
</constantValue>
</property>
Expand Down Expand Up @@ -329,6 +354,13 @@
</superClass>
</class>
</export>
<export name="SmartField">
<class name="SmartField">
<superClass>
<ref name="FormField" module="@eclipse-scout/core"></ref>
</superClass>
</class>
</export>
<export name="TableField">
<class name="TableField">
<superClass>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand Down Expand Up @@ -460,7 +460,10 @@ private static DataTypeSpi resolveTypeArgument(Element typeArgumentElement, Node
return createTypeOf(typeOfElement, moduleSpi);
}

return null;
return Strings.notBlank(typeArgumentElement.getTextContent())
.map(TestingNodeElementFactorySpi::newDataType)
.map(IDataType::spi)
.orElse(null);
}

@SuppressWarnings("TypeMayBeWeakened")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -15,6 +15,7 @@

import org.eclipse.scout.sdk.core.typescript.model.api.DataTypeNameEvaluator;
import org.eclipse.scout.sdk.core.typescript.model.api.IDataType;
import org.eclipse.scout.sdk.core.typescript.model.api.IES6Class;

public class ES6ImportValidator implements IES6ImportValidator {

Expand Down Expand Up @@ -54,6 +55,9 @@ protected String computeUniqueNameAndRegisterUsage(IDataType type) {
if (isBuiltInType(name)) {
return name; // no import required
}
if (type instanceof IES6Class es6Class) {
type = es6Class.withoutTypeArguments();
}
var collector = importCollector();
var usedNamesForSource = collector.usedNames();
if (!usedNamesForSource.contains(name)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -11,6 +11,7 @@ package org.eclipse.scout.sdk.s2i.model.typescript

import com.intellij.lang.javascript.psi.*
import com.intellij.lang.javascript.psi.ecma6.TypeScriptAsExpression
import com.intellij.lang.javascript.psi.ecma6.TypeScriptTypeArgumentList
import com.intellij.lang.javascript.psi.util.ExpressionUtil
import org.eclipse.scout.sdk.core.typescript.model.api.IConstantValue
import org.eclipse.scout.sdk.core.typescript.model.api.IDataType
Expand All @@ -21,6 +22,7 @@ import org.eclipse.scout.sdk.core.typescript.model.spi.NodeElementSpi
import org.eclipse.scout.sdk.core.util.FinalValue
import org.eclipse.scout.sdk.s2i.model.typescript.util.DataTypeSpiUtils
import org.eclipse.scout.sdk.s2i.resolveLocalPath
import org.eclipse.scout.sdk.s2i.util.compat.CompatibilityMethodCaller
import java.math.BigDecimal
import java.math.BigInteger
import java.nio.file.Path
Expand All @@ -38,21 +40,43 @@ open class IdeaConstantValue(val ideaModule: IdeaNodeModule, internal val elemen
private val m_dataType = FinalValue<IDataType?>()

fun unwrappedElement() = m_unwrappedElement.computeIfAbsentAndGet {
val unwrappedElement = unwrapTypeScriptAsExpression(element)
var unwrappedElement = unwrapJSParenthesizedExpression(element)
unwrappedElement = unwrapTypeScriptAsExpression(unwrappedElement)
unwrappedElement = unwrapTypeScriptExpressionWithTypeArguments(unwrappedElement)
unwrapJSNewExpression(unwrappedElement)
}

override fun containingFile(): Optional<Path> = m_containingFile.computeIfAbsentAndGet { Optional.ofNullable(element?.containingFile?.virtualFile?.resolveLocalPath()) }

protected fun unwrapJSParenthesizedExpression(element: JSElement?) = if (element is JSParenthesizedExpression) element.innerExpression else element

protected fun unwrapTypeScriptAsExpression(element: JSElement?) = if (element is TypeScriptAsExpression) element.expression else element

protected fun unwrapTypeScriptExpressionWithTypeArguments(element: JSElement?) = CompatibilityMethodCaller<JSElement?>()
.withCandidate("com.intellij.lang.javascript.psi.TypeScriptExpressionWithTypeArguments", "getExpression") {
// for IJ >= 2023.2 a JSReferenceExpression and its TypeScriptTypeArgumentList is wrapped in a TypeScriptExpressionWithTypeArguments => unwrap it and return its expression
if (it.descriptor.resolvedClass().isInstance(element)) {
return@withCandidate it.invoke(element)
}
element
}
.withCandidate("java.lang.Object", "toString") {
// for IJ < 2023.2 TypeScriptExpressionWithTypeArguments does not exists => no need to unwrap anything
element
}
.invoke() ?: element

protected fun unwrapJSNewExpression(element: JSElement?) = if (element is JSNewExpression) element.methodExpression else element

fun referencedElement() = m_referencedElement.computeIfAbsentAndGet {
(unwrappedElement() as? JSReferenceExpression)?.let { ideaModule.resolveReferencedElement(it) }
}

fun referencedES6Class() = m_referencedES6Class.computeIfAbsentAndGet { referencedElement() as? ES6ClassSpi }
fun referencedES6Class() = m_referencedES6Class.computeIfAbsentAndGet {
val referencedClass = referencedElement() as? ES6ClassSpi ?: return@computeIfAbsentAndGet null
val typeArgumentList = unwrappedElement()?.nextSibling as? TypeScriptTypeArgumentList ?: return@computeIfAbsentAndGet referencedClass
DataTypeSpiUtils.resolveTypeArguments(referencedClass, typeArgumentList.typeArguments, ideaModule)
}

fun referencedConstantValue() = m_referencedConstantValue.computeIfAbsentAndGet {
referencedES6Class()?.let { return@computeIfAbsentAndGet null }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand Down Expand Up @@ -205,7 +205,7 @@ class IdeaNodeModule(val moduleInventory: IdeaNodeModules, internal val nodeModu

internal fun resolveExportTarget(element: PsiElement): List<JSElement> {
var candidate: JSElement? = element as? JSElement
if (element.node.elementType == JSTokenTypes.EXPORT_KEYWORD) {
if (element.node?.elementType == JSTokenTypes.EXPORT_KEYWORD) {
candidate = element.parent as? JSElement
}
if (candidate is JSAttributeList) {
Expand Down
Loading

0 comments on commit b200273

Please sign in to comment.