From d016155709f8bdfe9f368bcf9b36979d9e51b57b Mon Sep 17 00:00:00 2001 From: David Thompson Date: Wed, 18 Dec 2024 17:25:19 -0500 Subject: [PATCH] Completion for `import module |;` This is new Java 23 syntax. This PR should make 6 new tests pass (mostly from the java 23 completion suite) This PR also incorporates https://github.com/eclipse-jdt/eclipse.jdt.core/pull/3479, I forget specifically why, but it's needed. - Fixes to the javac AST converter - I did a hack in the converter to recover the following case, since javac doesn't handle it currently: ```java import module public class HelloWorld { } ``` - Adjust module completion relevance based on if it's required by the current module Signed-off-by: David Thompson --- .../eclipse/jdt/core/dom/JavacConverter.java | 27 ++ .../internal/javac/JavacProblemConverter.java | 1 + .../codeassist/DOMCompletionEngine.java | 292 +++++++++++------- .../jdt/core/dom/RequiresDirective.java | 2 +- 4 files changed, 216 insertions(+), 106 deletions(-) diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/core/dom/JavacConverter.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/core/dom/JavacConverter.java index d8f9bd5740a..04f6e25d40a 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/core/dom/JavacConverter.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/core/dom/JavacConverter.java @@ -93,6 +93,7 @@ import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; import com.sun.tools.javac.tree.JCTree.JCModifiers; import com.sun.tools.javac.tree.JCTree.JCModuleDecl; +import com.sun.tools.javac.tree.JCTree.JCModuleImport; import com.sun.tools.javac.tree.JCTree.JCNewArray; import com.sun.tools.javac.tree.JCTree.JCNewClass; import com.sun.tools.javac.tree.JCTree.JCOpens; @@ -192,6 +193,9 @@ void populateCompilationUnit(CompilationUnit res, JCCompilationUnit javacCompila res.setModule(convert(javacCompilationUnit.getModuleDecl())); } javacCompilationUnit.getImports().stream().filter(imp -> imp instanceof JCImport).map(jc -> convert((JCImport)jc)).forEach(res.imports()::add); + if (this.ast.apiLevel >= AST.JLS23_INTERNAL) { + javacCompilationUnit.getImports().stream().filter(imp -> imp instanceof JCModuleImport).map(jc -> convert((JCModuleImport)jc)).forEach(res.imports()::add); + } javacCompilationUnit.getTypeDecls().stream() .map(n -> convertBodyDeclaration(n, res)) .filter(Objects::nonNull) @@ -394,6 +398,14 @@ private ImportDeclaration convert(JCImport javac) { if (select.getIdentifier().contentEquals("*")) { res.setOnDemand(true); res.setName(toName(select.getExpression())); + } else if (this.ast.apiLevel >= AST.JLS23_INTERNAL && select.selected.toString().equals("module") && select.name.toString().equals("")) { + // it's a broken module import + var moduleModifier = this.ast.newModifier(ModifierKeyword.MODULE_KEYWORD); + res.modifiers().add(moduleModifier); + Name name = new SimpleName(this.ast); + name.setSourceRange(res.getStartPosition() + res.getLength() + 1, 0); + res.setName(name); + res.setSourceRange(res.getStartPosition(), res.getLength() + 1); } else { res.setName(toName(select)); } @@ -423,6 +435,21 @@ private ImportDeclaration convert(JCImport javac) { } return res; } + + private ImportDeclaration convert(JCModuleImport javac) { + ImportDeclaration res = this.ast.newImportDeclaration(); + commonSettings(res, javac); + var moduleModifier = this.ast.newModifier(ModifierKeyword.MODULE_KEYWORD); + res.modifiers().add(moduleModifier); + if (javac.isStatic()) { + if( this.ast.apiLevel != AST.JLS2_INTERNAL) { + res.setStatic(true); + } + } + var select = javac.getQualifiedIdentifier(); + res.setName(toName(select)); + return res; + } void commonSettings(ASTNode res, JCTree javac) { if( javac != null ) { diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacProblemConverter.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacProblemConverter.java index 9c8860328d8..9c4be2737b1 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacProblemConverter.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacProblemConverter.java @@ -1080,6 +1080,7 @@ yield switch (rootCauseCode) { case "compiler.err.void.not.allowed.here" -> IProblem.ParameterMismatch; case "compiler.err.abstract.cant.be.accessed.directly" -> IProblem.DirectInvocationOfAbstractMethod; case "compiler.warn.annotation.method.not.found" -> IProblem.UndefinedAnnotationMember; + case "compiler.err.import.module.not.found" -> IProblem.UndefinedModule; default -> { ILog.get().error("Could not accurately convert diagnostic (" + diagnostic.getCode() + ")\n" + diagnostic); if (diagnostic.getKind() == javax.tools.Diagnostic.Kind.ERROR && diagnostic.getCode().startsWith("compiler.err")) { diff --git a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/DOMCompletionEngine.java b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/DOMCompletionEngine.java index 2bd604c4235..04f9bb1cdce 100644 --- a/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/DOMCompletionEngine.java +++ b/org.eclipse.jdt.core/codeassist/org/eclipse/jdt/internal/codeassist/DOMCompletionEngine.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.core.*; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.core.dom.*; +import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword; import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants; import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.SearchEngine; @@ -452,6 +453,17 @@ public void run() { } } } + if (context.getParent() instanceof ImportDeclaration importDeclaration + && context.getAST().apiLevel() >= AST.JLS23 + && this.modelUnit.getJavaProject().getOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, true).equals(JavaCore.ENABLED)) { + for (Modifier modifier : (List)importDeclaration.modifiers()) { + if (modifier.getKeyword() == ModifierKeyword.MODULE_KEYWORD) { + findModules(this.qualifiedPrefix.toCharArray(), this.modelUnit.getJavaProject(), this.assistOptions, Collections.emptySet()); + suggestDefaultCompletions = false; + break; + } + } + } } if (context instanceof AbstractTypeDeclaration typeDecl) { if (this.cuBuffer != null) { @@ -496,105 +508,115 @@ public void run() { } } if (context instanceof QualifiedName qualifiedName) { - IBinding qualifiedNameBinding = qualifiedName.getQualifier().resolveBinding(); - if (qualifiedNameBinding instanceof ITypeBinding qualifierTypeBinding && !qualifierTypeBinding.isRecovered()) { - processMembers(qualifiedName, qualifierTypeBinding, specificCompletionBindings, true); - publishFromScope(specificCompletionBindings); - int startPos = this.offset; - int endPos = this.offset; - if ((qualifiedName.getName().getFlags() & ASTNode.MALFORMED) != 0) { - startPos = qualifiedName.getName().getStartPosition(); - endPos = startPos + qualifiedName.getName().getLength(); - } - - if (!(this.toComplete instanceof Type)) { - ITypeBinding currentTypeBinding = DOMCompletionUtil.findParentTypeDeclaration(context).resolveBinding(); - if (currentTypeBinding.isSubTypeCompatible(qualifierTypeBinding)) { - if(!isFailedMatch(this.prefix.toCharArray(), Keywords.THIS)) { - this.requestor.accept(createKeywordProposal(Keywords.THIS, startPos, endPos)); + ImportDeclaration importDecl = (ImportDeclaration)DOMCompletionUtil.findParent(context, new int[] { ASTNode.IMPORT_DECLARATION }); + if (importDecl != null + && importDecl.getAST().apiLevel() >= AST.JLS23 + && this.modelUnit.getJavaProject().getOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, true).equals(JavaCore.ENABLED) + && importDecl.modifiers().stream().anyMatch(node -> node instanceof Modifier modifier && modifier.getKeyword() == ModifierKeyword.MODULE_KEYWORD)) { + findModules((this.qualifiedPrefix + "." + this.prefix).toCharArray(), this.modelUnit.getJavaProject(), this.assistOptions, Collections.emptySet()); //$NON-NLS-1$ + suggestDefaultCompletions = false; + } else { + IBinding qualifiedNameBinding = qualifiedName.getQualifier().resolveBinding(); + if (qualifiedNameBinding instanceof ITypeBinding qualifierTypeBinding && !qualifierTypeBinding.isRecovered()) { + processMembers(qualifiedName, qualifierTypeBinding, specificCompletionBindings, true); + publishFromScope(specificCompletionBindings); + int startPos = this.offset; + int endPos = this.offset; + if ((qualifiedName.getName().getFlags() & ASTNode.MALFORMED) != 0) { + startPos = qualifiedName.getName().getStartPosition(); + endPos = startPos + qualifiedName.getName().getLength(); + } + if (!(this.toComplete instanceof Type)) { + ITypeBinding currentTypeBinding = DOMCompletionUtil.findParentTypeDeclaration(context).resolveBinding(); + if (currentTypeBinding.isSubTypeCompatible(qualifierTypeBinding)) { + if (!isFailedMatch(this.prefix.toCharArray(), Keywords.THIS)) { + this.requestor.accept(createKeywordProposal(Keywords.THIS, startPos, endPos)); + } + if (!isFailedMatch(this.prefix.toCharArray(), Keywords.SUPER)) { + this.requestor.accept(createKeywordProposal(Keywords.SUPER, startPos, endPos)); + } } - if (!isFailedMatch(this.prefix.toCharArray(), Keywords.SUPER)) { - this.requestor.accept(createKeywordProposal(Keywords.SUPER, startPos, endPos)); + if (!isFailedMatch(this.prefix.toCharArray(), Keywords.CLASS)) { + this.requestor.accept(createClassKeywordProposal(qualifierTypeBinding, startPos, endPos)); } } - if (!isFailedMatch(this.prefix.toCharArray(), Keywords.CLASS)) { - this.requestor.accept(createClassKeywordProposal(qualifierTypeBinding, startPos, endPos)); - } - } - suggestDefaultCompletions = false; - } else if (qualifiedNameBinding instanceof IPackageBinding qualifierPackageBinding) { - if (!qualifierPackageBinding.isRecovered()) { - // start of a known package - suggestPackages(); - // suggests types in the package - suggestTypesInPackage(qualifierPackageBinding.getName()); suggestDefaultCompletions = false; - } else { - // likely the start of an incomplete field/method access - Bindings tempScope = new Bindings(); - scrapeAccessibleBindings(tempScope); - Optional potentialBinding = tempScope.all() // - .filter(binding -> { - IJavaElement elt = binding.getJavaElement(); - if (elt == null) { - return false; - } - return elt.getElementName().equals(qualifiedName.getQualifier().toString()); - }) // - .map(binding -> { - if (binding instanceof IVariableBinding variableBinding) { - return variableBinding.getType(); - } else if (binding instanceof ITypeBinding typeBinding) { - return typeBinding; - } - throw new IllegalStateException("method, type var, etc. are likely not interpreted as a package"); //$NON-NLS-1$ - }) - .map(ITypeBinding.class::cast) - .findFirst(); - if (potentialBinding.isPresent()) { - processMembers(qualifiedName, potentialBinding.get(), specificCompletionBindings, false); - publishFromScope(specificCompletionBindings); - suggestDefaultCompletions = false; - } else { - // maybe it is actually a package? + } else if (qualifiedNameBinding instanceof IPackageBinding qualifierPackageBinding) { + if (!qualifierPackageBinding.isRecovered()) { + // start of a known package suggestPackages(); // suggests types in the package suggestTypesInPackage(qualifierPackageBinding.getName()); suggestDefaultCompletions = false; - } - } - } else if (qualifiedNameBinding instanceof IVariableBinding variableBinding) { - ITypeBinding typeBinding = variableBinding.getType(); - processMembers(qualifiedName, typeBinding, specificCompletionBindings, false); - publishFromScope(specificCompletionBindings); - suggestDefaultCompletions = false; - } else { - // UnimportedType.| - List foundTypes = findTypes(qualifiedName.getQualifier().toString(), null).toList(); - // HACK: We requested exact matches from the search engine but some results aren't exact - foundTypes = foundTypes.stream().filter(type -> type.getElementName().equals(qualifiedName.getQualifier().toString())).toList(); - if (!foundTypes.isEmpty()) { - IType firstType = foundTypes.get(0); - ASTParser parser = ASTParser.newParser(AST.getJLSLatest()); - parser.setProject(this.modelUnit.getJavaProject()); - IBinding[] descendantBindings = parser.createBindings(new IType[] { firstType }, new NullProgressMonitor()); - if (descendantBindings.length == 1) { - ITypeBinding qualifierTypeBinding = (ITypeBinding)descendantBindings[0]; - processMembers(qualifiedName, qualifierTypeBinding, specificCompletionBindings, true); - publishFromScope(specificCompletionBindings); - int startPos = this.offset; - int endPos = this.offset; - if ((qualifiedName.getName().getFlags() & ASTNode.MALFORMED) != 0) { - startPos = qualifiedName.getName().getStartPosition(); - endPos = startPos + qualifiedName.getName().getLength(); + } else { + // likely the start of an incomplete field/method access + Bindings tempScope = new Bindings(); + scrapeAccessibleBindings(tempScope); + Optional potentialBinding = tempScope.all() // + .filter(binding -> { + IJavaElement elt = binding.getJavaElement(); + if (elt == null) { + return false; + } + return elt.getElementName().equals(qualifiedName.getQualifier().toString()); + }) // + .map(binding -> { + if (binding instanceof IVariableBinding variableBinding) { + return variableBinding.getType(); + } else if (binding instanceof ITypeBinding typeBinding) { + return typeBinding; + } + throw new IllegalStateException( + "method, type var, etc. are likely not interpreted as a package"); //$NON-NLS-1$ + }) // + .map(ITypeBinding.class::cast) // + .findFirst(); + if (potentialBinding.isPresent()) { + processMembers(qualifiedName, potentialBinding.get(), specificCompletionBindings, + false); + publishFromScope(specificCompletionBindings); + suggestDefaultCompletions = false; + } else { + // maybe it is actually a package? + suggestPackages(); + // suggests types in the package + suggestTypesInPackage(qualifierPackageBinding.getName()); + suggestDefaultCompletions = false; } - if (!(this.toComplete instanceof Type) && !isFailedMatch(this.prefix.toCharArray(), Keywords.CLASS)) { - this.requestor.accept(createClassKeywordProposal(qualifierTypeBinding, startPos, endPos)); + } + } else if (qualifiedNameBinding instanceof IVariableBinding variableBinding) { + ITypeBinding typeBinding = variableBinding.getType(); + processMembers(qualifiedName, typeBinding, specificCompletionBindings, false); + publishFromScope(specificCompletionBindings); + suggestDefaultCompletions = false; + } else { + // UnimportedType.| + List foundTypes = findTypes(qualifiedName.getQualifier().toString(), null).toList(); + // HACK: We requested exact matches from the search engine but some results aren't exact + foundTypes = foundTypes.stream().filter(type -> type.getElementName().equals(qualifiedName.getQualifier().toString())).toList(); + if (!foundTypes.isEmpty()) { + IType firstType = foundTypes.get(0); + ASTParser parser = ASTParser.newParser(AST.getJLSLatest()); + parser.setProject(this.modelUnit.getJavaProject()); + IBinding[] descendantBindings = parser.createBindings(new IType[] { firstType }, new NullProgressMonitor()); + if (descendantBindings.length == 1) { + ITypeBinding qualifierTypeBinding = (ITypeBinding)descendantBindings[0]; + processMembers(qualifiedName, qualifierTypeBinding, specificCompletionBindings, true); + publishFromScope(specificCompletionBindings); + int startPos = this.offset; + int endPos = this.offset; + if ((qualifiedName.getName().getFlags() & ASTNode.MALFORMED) != 0) { + startPos = qualifiedName.getName().getStartPosition(); + endPos = startPos + qualifiedName.getName().getLength(); + } + if (!(this.toComplete instanceof Type) && !isFailedMatch(this.prefix.toCharArray(), Keywords.CLASS)) { + this.requestor.accept(createClassKeywordProposal(qualifierTypeBinding, startPos, endPos)); + } } } + suggestDefaultCompletions = false; } - suggestDefaultCompletions = false; } } if (context instanceof SuperFieldAccess superFieldAccess) { @@ -696,6 +718,13 @@ public void run() { completeJavadocInlineTags(tagElement); suggestDefaultCompletions = false; } + if (context instanceof ImportDeclaration) { + if (context.getAST().apiLevel() >= AST.JLS23 + && this.modelUnit.getJavaProject().getOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, true).equals(JavaCore.ENABLED)) { + findModules(this.qualifiedPrefix.toCharArray(), this.modelUnit.getJavaProject(), this.assistOptions, Collections.emptySet()); + suggestDefaultCompletions = false; + } + } // check for accessible bindings to potentially turn into completions. // currently, this is always run, even when not using the default completion, @@ -2306,8 +2335,8 @@ private int computeRelevanceForExpectingType(ITypeBinding proposalType){ return 0; } - private HashSet getAllJarModuleNames(IJavaProject project) { - HashSet modules = new HashSet<>(); + private Map getAllJarModuleNames(IJavaProject project) { + Map modules = new HashMap<>(); try { for (IPackageFragmentRoot root : project.getAllPackageFragmentRoots()) { if (root instanceof JarPackageFragmentRoot) { @@ -2315,7 +2344,7 @@ private HashSet getAllJarModuleNames(IJavaProject project) { desc = desc == null ? ((JarPackageFragmentRoot) root).getAutomaticModuleDescription() : desc; String name = desc != null ? desc.getElementName() : null; if (name != null && name.length() > 0) - modules.add(name); + modules.putIfAbsent(name, desc); } } } catch (JavaModelException e) { @@ -2328,8 +2357,7 @@ private void findModules(char[] prefix, IJavaProject project, AssistOptions opti if(this.requestor.isIgnored(CompletionProposal.MODULE_REF)) { return; } - - HashSet probableModules = new HashSet<>(); + HashMap probableModules = new HashMap<>(); ModuleSourcePathManager mManager = JavaModelManager.getModulePathManager(); JavaElementRequestor javaElementRequestor = new JavaElementRequestor(); try { @@ -2339,38 +2367,92 @@ private void findModules(char[] prefix, IJavaProject project, AssistOptions opti String name = module.getElementName(); if (name == null || name.equals("")) //$NON-NLS-1$ continue; - probableModules.add(name); + probableModules.putIfAbsent(name, module); } } catch (JavaModelException e) { // ignore the error } - probableModules.addAll(getAllJarModuleNames(project)); + probableModules.putAll(getAllJarModuleNames(project)); + Set requiredModules = collectRequiredModules(probableModules); + List removeList = new ArrayList<>(); if (prefix != CharOperation.ALL_PREFIX && prefix != null && prefix.length > 0) { - probableModules.removeIf(e -> CompletionEngine.isFailedMatch(prefix, e.toCharArray(), options)); + for (String key : probableModules.keySet()) { + if (CompletionEngine.isFailedMatch(prefix, key.toCharArray(), options)) { + removeList.add(key); + } + } + } + for (String key : removeList) { + probableModules.remove(key); + } + removeList.clear(); + for (String key : skip) { + probableModules.remove(key); + } + probableModules.entrySet().forEach(m -> this.requestor.accept(toModuleCompletion(m.getKey(), prefix, requiredModules))); + } + + /** + * Returns the list of modules required by the current module, including transitive ones. + * + * The current module and java.base included in the set. + * + * @param reachableModules the map of reachable modules + * @return the list of modules required by the current module, including transitive ones + */ + private Set collectRequiredModules(Map reachableModules) { + Set requiredModules = new HashSet<>(); + requiredModules.add("java.base"); //$NON-NLS-1$ + try { + IModuleDescription ownDescription = this.modelUnit.getJavaProject().getModuleDescription(); + if (ownDescription != null && !ownDescription.getElementName().isEmpty()) { + Deque moduleQueue = new ArrayDeque<>(); + requiredModules.add(ownDescription.getElementName()); + for (String moduleName : ownDescription.getRequiredModuleNames()) { + moduleQueue.add(moduleName); + } + while (!moduleQueue.isEmpty()) { + String top = moduleQueue.pollFirst(); + requiredModules.add(top); + if (reachableModules.containsKey(top)) { + for (String moduleName : reachableModules.get(top).getRequiredModuleNames()) { + if (!requiredModules.contains(moduleName)) { + moduleQueue.add(moduleName); + } + } + } + } + } else { + // working with the default module, so everything is required I think? + return reachableModules.keySet(); + } + } catch (JavaModelException e) { + // do nothing } - probableModules.removeIf(skip::contains); - probableModules.forEach(m -> this.requestor.accept(toModuleCompletion(m, prefix))); + return requiredModules; } - private CompletionProposal toModuleCompletion(String moduleName, char[] prefix) { + private CompletionProposal toModuleCompletion(String moduleName, char[] prefix, Set requiredModules) { + char[] completion = moduleName.toCharArray(); int relevance = CompletionEngine.computeBaseRelevance(); relevance += CompletionEngine.computeRelevanceForResolution(); relevance += this.nestedEngine.computeRelevanceForInterestingProposal(); relevance += this.nestedEngine.computeRelevanceForCaseMatching(prefix, completion); relevance += this.nestedEngine.computeRelevanceForQualification(true); - relevance += this.nestedEngine.computeRelevanceForRestrictions(IAccessRule.K_ACCESSIBLE); - InternalCompletionProposal proposal = new InternalCompletionProposal(CompletionProposal.MODULE_REF, - this.offset); + if (requiredModules.contains(moduleName)) { + relevance += CompletionEngine.computeRelevanceForRestrictions(IAccessRule.K_ACCESSIBLE); + } + InternalCompletionProposal proposal = createProposal(CompletionProposal.MODULE_REF); proposal.setModuleName(completion); proposal.setDeclarationSignature(completion); proposal.setCompletion(completion); - proposal.setReplaceRange( - this.toComplete instanceof SimpleName ? this.toComplete.getStartPosition() : this.offset, - DOMCompletionEngine.this.offset); + + // replacement range using import decl range: + ImportDeclaration importDecl = (ImportDeclaration) DOMCompletionUtil.findParent(this.toComplete, new int[] {ASTNode.IMPORT_DECLARATION}); + proposal.setReplaceRange(importDecl.getName().getStartPosition(), importDecl.getName().getStartPosition() + importDecl.getName().getLength()); + proposal.setRelevance(relevance); - proposal.completionEngine = this.nestedEngine; - proposal.nameLookup = this.nameEnvironment.nameLookup; return proposal; } diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/RequiresDirective.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/RequiresDirective.java index e141d8c2583..fc631eed614 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/RequiresDirective.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/RequiresDirective.java @@ -39,7 +39,7 @@ public class RequiresDirective extends ModuleDirective { * The module structural property of this node type (child type: {@link Name}). */ public static final ChildPropertyDescriptor NAME_PROPERTY = - new ChildPropertyDescriptor(RequiresDirective.class, "name", Name.class, OPTIONAL, NO_CYCLE_RISK); //$NON-NLS-1$ + new ChildPropertyDescriptor(RequiresDirective.class, "name", Name.class, MANDATORY, NO_CYCLE_RISK); //$NON-NLS-1$ /** * A list of property descriptors (element type: