blob: 9fa6c32bcfb8c8ed45ff97cb66dc98461dd9203b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 xored software, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* xored software, Inc. - initial API and Implementation (Alex Panchenko)
*******************************************************************************/
package org.eclipse.dltk.javascript.internal.core.codeassist;
import static org.eclipse.dltk.javascript.ast.Keywords.THIS;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.runtime.Platform;
import org.eclipse.dltk.annotations.Internal;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.codeassist.ScriptCompletionEngine;
import org.eclipse.dltk.compiler.CharOperation;
import org.eclipse.dltk.compiler.env.IModuleSource;
import org.eclipse.dltk.compiler.problem.IValidationStatus;
import org.eclipse.dltk.compiler.problem.ValidationStatus;
import org.eclipse.dltk.core.CompletionContext;
import org.eclipse.dltk.core.CompletionProposal;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.IAccessRule;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.internal.javascript.ti.FunctionMethod;
import org.eclipse.dltk.internal.javascript.ti.IReferenceAttributes;
import org.eclipse.dltk.internal.javascript.ti.ITypeInferenceContext;
import org.eclipse.dltk.internal.javascript.ti.PositionReachedException;
import org.eclipse.dltk.internal.javascript.ti.TypeInferencer2;
import org.eclipse.dltk.internal.javascript.typeinference.CompletionPath;
import org.eclipse.dltk.internal.javascript.validation.MemberValidationEvent;
import org.eclipse.dltk.javascript.ast.Identifier;
import org.eclipse.dltk.javascript.ast.Script;
import org.eclipse.dltk.javascript.ast.StringLiteral;
import org.eclipse.dltk.javascript.core.JavaScriptKeywords;
import org.eclipse.dltk.javascript.core.JavaScriptPlugin;
import org.eclipse.dltk.javascript.core.NodeFinder;
import org.eclipse.dltk.javascript.core.Types;
import org.eclipse.dltk.javascript.internal.core.codeassist.JavaScriptCompletionUtil.ExpressionContext;
import org.eclipse.dltk.javascript.internal.core.codeassist.JavaScriptCompletionUtil.ExpressionType;
import org.eclipse.dltk.javascript.parser.JavaScriptParserUtil;
import org.eclipse.dltk.javascript.typeinference.IValueCollection;
import org.eclipse.dltk.javascript.typeinference.IValueParent;
import org.eclipse.dltk.javascript.typeinference.IValueReference;
import org.eclipse.dltk.javascript.typeinference.ReferenceKind;
import org.eclipse.dltk.javascript.typeinfo.IRClassType;
import org.eclipse.dltk.javascript.typeinfo.IRElement;
import org.eclipse.dltk.javascript.typeinfo.IRFunctionType;
import org.eclipse.dltk.javascript.typeinfo.IRMember;
import org.eclipse.dltk.javascript.typeinfo.IRMethod;
import org.eclipse.dltk.javascript.typeinfo.IRParameter;
import org.eclipse.dltk.javascript.typeinfo.IRRecordMember;
import org.eclipse.dltk.javascript.typeinfo.IRRecordType;
import org.eclipse.dltk.javascript.typeinfo.IRSimpleType;
import org.eclipse.dltk.javascript.typeinfo.IRType;
import org.eclipse.dltk.javascript.typeinfo.IRTypeDeclaration;
import org.eclipse.dltk.javascript.typeinfo.ITypeNames;
import org.eclipse.dltk.javascript.typeinfo.ITypeSystem;
import org.eclipse.dltk.javascript.typeinfo.JSTypeSet;
import org.eclipse.dltk.javascript.typeinfo.MemberPredicate;
import org.eclipse.dltk.javascript.typeinfo.MemberPredicates;
import org.eclipse.dltk.javascript.typeinfo.RTypeMemberQuery;
import org.eclipse.dltk.javascript.typeinfo.RTypes;
import org.eclipse.dltk.javascript.typeinfo.TypeInfoManager;
import org.eclipse.dltk.javascript.typeinfo.TypeMode;
import org.eclipse.dltk.javascript.typeinfo.model.Member;
import org.eclipse.dltk.javascript.typeinfo.model.ParameterKind;
import org.eclipse.dltk.javascript.typeinfo.model.Type;
import org.eclipse.dltk.javascript.typeinfo.model.TypeKind;
import org.eclipse.dltk.javascript.validation.IValidatorExtension;
public class JavaScriptCompletionEngine2 extends ScriptCompletionEngine
implements JSCompletionEngine {
private int globalOptions = JSCompletionEngine.OPTION_ALL;
public int getGlobalOptions() {
return globalOptions;
}
public void setGlobalOptions(int value) {
this.globalOptions = value;
}
public void complete(final IModuleSource cu, final int position, int i) {
this.requestor.beginReporting();
final String content = cu.getSourceContents();
if (position < 0 || position > content.length()) {
return;
}
final TypeInferencer2 inferencer2 = new TypeInferencer2();
inferencer2.setModelElement(cu.getModelElement());
final Script script = JavaScriptParserUtil.parse(cu, null);
final NodeFinder nodeFinder = new NodeFinder(position, position);
nodeFinder.locate(script);
if (nodeFinder.getNode() instanceof StringLiteral) {
// don't complete inside string literals
return;
}
final PositionCalculator calculator = new PositionCalculator(content,
position, false);
final CompletionVisitor visitor = new CompletionVisitor(inferencer2,
position);
inferencer2.setVisitor(visitor);
if (cu instanceof org.eclipse.dltk.core.ISourceModule) {
inferencer2
.setModelElement((org.eclipse.dltk.core.ISourceModule) cu);
}
try {
inferencer2.doInferencing(script);
} catch (PositionReachedException e) {
// e.printStackTrace();
}
ITypeSystem.CURRENT.runWith(inferencer2, new Runnable() {
public void run() {
final CompletionPath path = new CompletionPath(calculator
.getCompletion());
final ASTNode node = nodeFinder.getNode();
if (node instanceof Identifier) {
setSourceRange(node.start(), node.end());
} else {
String lastSegment = path.lastSegment();
if (lastSegment == null)
lastSegment = "";
setSourceRange(position - lastSegment.length(), position);
}
final Reporter reporter = new Reporter(inferencer2, path
.lastSegment(), position, TypeInfoManager
.createExtensions(inferencer2,
IValidatorExtension.class, null));
if (calculator.isMember() && !path.isEmpty()
&& path.lastSegment() != null) {
doCompletionOnMember(inferencer2, visitor.getCollection(),
path, reporter);
} else {
doGlobalCompletion(visitor.getCollection(), reporter,
JavaScriptCompletionUtil.evaluateExpressionContext(
script, content, position));
}
}
});
this.requestor.endReporting();
}
public void completeTypes(ISourceModule module, TypeMode mode,
String prefix, int offset) {
final TypeInferencer2 inferencer2 = new TypeInferencer2();
inferencer2.setModelElement(module);
final Script script = JavaScriptParserUtil.parse(module, null);
inferencer2.doInferencing(script);
setSourceRange(offset - prefix.length(), offset);
doCompletionOnType(mode, new Reporter(inferencer2, prefix, offset,
Collections.<IValidatorExtension> emptyList()));
}
public void completeGlobals(ISourceModule module, final String prefix,
final int offset, boolean jsdoc) {
final CompletionContext completionContext = new CompletionContext();
completionContext.setOffset(offset);
completionContext.setDoc(jsdoc);
requestor.acceptContext(completionContext);
setSourceRange(offset - prefix.length(), offset);
final TypeInferencer2 inferencer2 = new TypeInferencer2();
inferencer2.setModelElement(module);
final CompletionVisitor visitor = new CompletionVisitor(inferencer2,
Integer.MAX_VALUE);
inferencer2.setVisitor(visitor);
final Script script = JavaScriptParserUtil.parse(module, null);
try {
inferencer2.doInferencing(script);
} catch (PositionReachedException e) {
// e.printStackTrace();
}
ITypeSystem.CURRENT.runWith(inferencer2, new Runnable() {
public void run() {
final Reporter reporter = new Reporter(inferencer2, prefix,
offset, TypeInfoManager.createExtensions(inferencer2,
IValidatorExtension.class, null));
doGlobalCompletion(visitor.getCollection(), reporter, null);
}
});
}
/**
* Generate completion proposals for the matching members from the specified
* {@link Iterable}.
*/
public void completeMembers(ISourceModule module, String prefix,
int offset, boolean jsdoc, Iterable<IRMember> memers) {
final CompletionContext completionContext = new CompletionContext();
completionContext.setOffset(offset);
completionContext.setDoc(jsdoc);
requestor.acceptContext(completionContext);
setSourceRange(offset - prefix.length(), offset);
final TypeInferencer2 inferencer2 = new TypeInferencer2();
inferencer2.setModelElement(module);
final Reporter reporter = new Reporter(inferencer2, prefix, offset,
TypeInfoManager.createExtensions(inferencer2,
IValidatorExtension.class, null));
for (IRMember member : memers) {
final String name = member.getName();
if (reporter.matches(name)) {
reporter.report(name, member);
}
}
}
private void doCompletionOnType(TypeMode mode, Reporter reporter) {
final ITypeInferenceContext context = reporter.context;
Set<String> typeNames = context.listTypes(mode, reporter.getPrefix());
for (String typeName : typeNames) {
final Type type = context.getType(typeName);
if (type != null && type.isVisible()) {
reporter.reportTypeRef(type);
}
}
}
private static boolean exists(IValueParent item) {
if (item instanceof IValueReference) {
return ((IValueReference) item).exists();
} else {
return true;
}
}
/**
* @param context
* @param collection
* @param startPart
*/
@Internal
void doCompletionOnMember(ITypeInferenceContext context,
IValueCollection collection, CompletionPath path, Reporter reporter) {
IValueParent item = collection;
for (int i = 0; i < path.segmentCount() - 1; ++i) {
if (path.isName(i)) {
final String segment = path.segment(i);
if (THIS.equals(segment) && item instanceof IValueCollection) {
item = ((IValueCollection) item).getThis();
} else {
item = item.getChild(segment);
}
if (!exists(item))
break;
} else if (path.isFunction(i)) {
item = item.getChild(IValueReference.FUNCTION_OP);
if (!exists(item))
break;
} else if (path.isObject(i)) {
item = item.getChild(ITypeNames.OBJECT).getChild(
IValueReference.FUNCTION_OP);
break;
} else {
assert path.isArray(i);
item = item.getChild(IValueReference.ARRAY_OP);
if (!exists(item))
break;
}
}
if (item != null && exists(item)) {
reportItems(reporter, item);
}
}
protected void reportItems(Reporter reporter, IValueParent item) {
reporter.report(item);
if (item instanceof IValueCollection) {
IValueCollection coll = (IValueCollection) item;
for (;;) {
coll = coll.getParent();
if (coll == null)
break;
reporter.report(coll);
}
} else if (item instanceof IValueReference) {
reporter.reportValueTypeMembers((IValueReference) item);
}
}
protected void reportGlobals(Reporter reporter) {
final ITypeInferenceContext context = reporter.context;
final Set<String> globals = context.listGlobals(reporter.getPrefix());
for (String global : globals) {
if (reporter.canReport(global)) {
IRMember element = context.resolve(global);
if (element != null && element.isVisible()) {
reporter.report(global, element);
}
} else if (global.lastIndexOf('.') != -1) {
Type type = context.getType(global);
if (type != null && type.isVisible()
&& type.getKind() != TypeKind.UNKNOWN) {
reporter.reportTypeRef(type);
}
}
}
}
private class Reporter {
final ITypeInferenceContext context;
final char[] prefix;
private final String prefixStr;
final int position;
final Set<String> processed = new HashSet<String>();
final boolean camelCase = DLTKCore.ENABLED.equals(DLTKCore
.getOption(DLTKCore.CODEASSIST_CAMEL_CASE_MATCH));
final boolean visibilityCheck = DLTKCore.ENABLED.equals(Platform
.getPreferencesService().getString(JavaScriptPlugin.PLUGIN_ID,
DLTKCore.CODEASSIST_VISIBILITY_CHECK, null, null));
final IValidatorExtension[] extensions;
public Reporter(ITypeInferenceContext context, String prefix,
int position, List<IValidatorExtension> extensions) {
this.context = context;
this.prefixStr = prefix != null ? prefix : "";
this.prefix = prefixStr.toCharArray();
this.position = position;
if (!extensions.isEmpty()) {
this.extensions = extensions
.toArray(new IValidatorExtension[extensions.size()]);
} else {
this.extensions = null;
}
}
@SuppressWarnings("unused")
public void ignore(String generatedIdentifier) {
processed.add(generatedIdentifier);
}
public String getPrefix() {
return prefixStr;
}
public int getPosition() {
return position;
}
public void report(String name, IRElement element) {
if (element instanceof IRMember && processed.add(name)) {
reportMember((IRMember) element, name, false);
}
}
public boolean canReport(String name) {
return matches(name) && !processed.contains(name);
}
boolean matches(String name) {
return CharOperation.prefixEquals(prefix, name, false) || camelCase
&& CharOperation.camelCaseMatch(prefix, name.toCharArray());
}
private MemberValidationEvent memberValidationEvent;
public void report(IValueParent item) {
final Set<String> deleted = item.getDeletedChildren();
CHILDREN: for (String childName : item.getDirectChildren()) {
if (childName.equals(IValueReference.FUNCTION_OP))
continue;
if (!deleted.contains(childName) && matches(childName)
&& processed.add(childName)) {
IValueReference child = item.getChild(childName);
if (child.exists()) {
if (visibilityCheck && extensions != null) {
if (memberValidationEvent == null) {
memberValidationEvent = new MemberValidationEvent();
}
memberValidationEvent.set(child, null);
for (IValidatorExtension extension : extensions) {
final IValidationStatus status = extension
.validateAccessibility(memberValidationEvent);
if (status != null) {
if (status == ValidationStatus.OK) {
break;
} else {
continue CHILDREN;
}
}
}
}
reportReference(child);
}
}
}
}
public void reportValueTypeMembers(IValueReference valueRef) {
final RTypeMemberQuery typeQuery = new RTypeMemberQuery();
final Set<IRMember> members = new HashSet<IRMember>();
collectTypes(valueRef.getDeclaredTypes(), typeQuery, members,
valueRef);
collectTypes(valueRef.getTypes(), typeQuery, members, valueRef);
for (IRMember member : members) {
if (member.isVisible() && matches(member.getName())) {
reportMember(member, member.getName(), true);
}
}
for (IRMember member : typeQuery.ignoreDuplicates(processed)) {
if (member.isVisible() && matches(member.getName())) {
reportMember(member, member.getName(),
typeQuery.contains(member.getDeclaringType()));
}
}
}
protected void collectTypes(final JSTypeSet types,
final RTypeMemberQuery typeQuery,
final Collection<IRMember> members, IValueReference valueRef) {
for (IRType type : types) {
if (type instanceof IRClassType) {
final IRTypeDeclaration t = ((IRClassType) type)
.getDeclaration();
if (t != null) {
final MemberPredicate predicate = t.getSource()
.memberPredicateFor(type,
MemberPredicates.STATIC);
typeQuery.add(t, predicate);
if (t.getSource().hasPrototype()
&& predicate
.isCompatibleWith(MemberPredicates.STATIC)) {
Type prototypeType = t.getSource()
.getPrototypeType();
if (prototypeType == Types.FUNCTION
&& !canInstantiate(t.getSource(), valueRef)) {
prototypeType = Types.OBJECT;
}
typeQuery.add(context.convert(prototypeType),
MemberPredicates.NON_STATIC);
}
} else {
typeQuery.add(RTypes.FUNCTION.getDeclaration(),
MemberPredicates.NON_STATIC);
}
} else if (type instanceof IRSimpleType) {
final IRTypeDeclaration t = ((IRSimpleType) type)
.getDeclaration();
typeQuery.add(
t,
t.getSource().memberPredicateFor(type,
MemberPredicates.NON_STATIC));
} else if (type instanceof IRRecordType) {
members.addAll(((IRRecordType) type).getMembers());
typeQuery.add(RTypes.OBJECT.getDeclaration(),
MemberPredicates.NON_STATIC);
} else if (type instanceof IRFunctionType) {
final IRFunctionType functionType = (IRFunctionType) type;
members.add(FunctionMethod.apply.create(functionType));
members.add(FunctionMethod.call.create(functionType));
typeQuery.add(RTypes.FUNCTION.getDeclaration(),
new MemberPredicate() {
public boolean isCompatibleWith(
MemberPredicate predicate) {
return MemberPredicates.NON_STATIC == predicate;
}
public boolean evaluate(Member member) {
return MemberPredicates.NON_STATIC
.evaluate(member)
&& !FunctionMethod.apply
.test(member.getName())
&& !FunctionMethod.call.test(member
.getName());
}
public boolean evaluate(IRMember member) {
return member.getSource() instanceof Member
&& evaluate((Member) member
.getSource());
}
});
} else if (type.isJavaScriptObject()) {
typeQuery.add(RTypes.OBJECT.getDeclaration(),
MemberPredicates.NON_STATIC);
}
}
}
private boolean canInstantiate(Type type, IValueReference ref) {
if (extensions != null) {
for (IValidatorExtension extension : extensions) {
final IValidationStatus status = extension.canInstantiate(
type, ref);
if (status != null && status != ValidationStatus.OK) {
return false;
}
}
}
return true;
}
/**
* @param member
*/
private void reportMember(IRMember member, String memberName,
boolean important) {
if (visibilityCheck && extensions != null
&& member.getSource() instanceof Member) {
final Member source = (Member) member.getSource();
for (IValidatorExtension extension : extensions) {
final IValidationStatus status = extension
.validateAccessibility(source);
if (status != null) {
if (status == ValidationStatus.OK) {
break;
} else {
return;
}
}
}
}
boolean isFunction = member instanceof IRMethod
|| (member instanceof IRRecordMember && member.getType() instanceof IRFunctionType);
CompletionProposal proposal = CompletionProposal.create(
isFunction ? CompletionProposal.METHOD_REF
: CompletionProposal.FIELD_REF, position);
int relevance = computeBaseRelevance();
// if (important) {
// relevance += RelevanceConstants.R_NON_INHERITED;
// }
relevance += computeRelevanceForInterestingProposal();
relevance += computeRelevanceForCaseMatching(prefix, memberName);
relevance += computeRelevanceForRestrictions(IAccessRule.K_ACCESSIBLE);
proposal.setRelevance(relevance);
proposal.setCompletion(isFunction ? memberName + "()" : memberName);
proposal.setName(memberName);
proposal.setExtraInfo(member.getSource());
if (isFunction) {
List<IRParameter> parameters = null;
if (member.getType() instanceof IRFunctionType) {
parameters = ((IRFunctionType) member.getType())
.getParameters();
} else {
parameters = ((IRMethod) member).getParameters();
}
int paramCount = parameters.size();
if (paramCount > 0) {
final String[] params = new String[paramCount];
for (int i = 0; i < paramCount; ++i) {
params[i] = parameters.get(i).getName();
if (params[i] == null
&& parameters.get(i).getType() != null)
params[i] = parameters.get(i).getType().getName();
if (params[i] == null)
params[i] = "anon_" + i;
}
proposal.setParameterNames(params);
if (parameters.get(paramCount - 1).getKind() != ParameterKind.NORMAL) {
int requiredCount = parameters.size();
while (requiredCount > 0
&& parameters.get(requiredCount - 1).getKind() != ParameterKind.NORMAL) {
--requiredCount;
}
if (requiredCount == 0
&& parameters.get(requiredCount).getKind() == ParameterKind.VARARGS) {
++requiredCount; // heuristic...
}
if (requiredCount != paramCount) {
proposal.setAttribute(
CompletionProposal.ATTR_REQUIRED_PARAM_COUNT,
requiredCount);
}
}
}
}
accept(proposal);
}
/**
* @param reference
*/
private void reportReference(IValueReference reference) {
Object element = reference
.getAttribute(IReferenceAttributes.ELEMENT);
if (element instanceof IRMember) {
reportMember((IRMember) element,
((IRMember) element).getName(), true);
} else {
int proposalKind = CompletionProposal.FIELD_REF;
final ReferenceKind kind = reference.getKind();
if (reference.getAttribute(IReferenceAttributes.PHANTOM, true) == null
&& (kind == ReferenceKind.FUNCTION || reference
.hasChild(IValueReference.FUNCTION_OP))) {
proposalKind = CompletionProposal.METHOD_REF;
} else if (kind == ReferenceKind.LOCAL) {
proposalKind = CompletionProposal.LOCAL_VARIABLE_REF;
}
CompletionProposal proposal = CompletionProposal.create(
proposalKind, position);
int relevance = computeBaseRelevance();
relevance += computeRelevanceForInterestingProposal();
relevance += computeRelevanceForCaseMatching(prefix,
reference.getName());
relevance += computeRelevanceForRestrictions(IAccessRule.K_ACCESSIBLE);
proposal.setRelevance(relevance);
proposal.setCompletion(proposalKind == CompletionProposal.METHOD_REF ? reference
.getName() + "()"
: reference.getName());
proposal.setName(reference.getName());
proposal.setExtraInfo(reference);
if (proposalKind == CompletionProposal.METHOD_REF) {
final IRMethod method = (IRMethod) reference.getAttribute(
IReferenceAttributes.R_METHOD, true);
if (method != null) {
int paramCount = method.getParameterCount();
if (paramCount > 0) {
final String[] params = new String[paramCount];
for (int i = 0; i < paramCount; ++i) {
params[i] = method.getParameters().get(i)
.getName();
}
proposal.setParameterNames(params);
if (method.getParameters().get(paramCount - 1)
.getKind() != ParameterKind.NORMAL) {
int requiredCount = method.getParameters()
.size();
while (requiredCount > 0
&& method.getParameters()
.get(requiredCount - 1)
.getKind() != ParameterKind.NORMAL) {
--requiredCount;
}
if (requiredCount == 0
&& method.getParameters()
.get(requiredCount).getKind() == ParameterKind.VARARGS) {
++requiredCount; // heuristic...
}
if (requiredCount != paramCount) {
proposal.setAttribute(
CompletionProposal.ATTR_REQUIRED_PARAM_COUNT,
requiredCount);
}
}
}
}
}
accept(proposal);
}
}
public void reportTypeRef(Type type) {
if (!processed.add(type.getName())) {
return;
}
CompletionProposal proposal = CompletionProposal.create(
CompletionProposal.TYPE_REF, position);
int relevance = computeBaseRelevance();
relevance += computeRelevanceForInterestingProposal();
relevance += computeRelevanceForCaseMatching(prefix, type.getName());
relevance += computeRelevanceForRestrictions(IAccessRule.K_ACCESSIBLE);
proposal.setRelevance(relevance);
proposal.setCompletion(type.getName());
proposal.setName(type.getName());
proposal.setExtraInfo(type);
accept(proposal);
}
}
/**
* @param context
* @param collection
* @param reporter
* @param expressionContext
*/
@Internal
void doGlobalCompletion(IValueCollection collection, Reporter reporter,
ExpressionContext expressionContext) {
if (expressionContext != null
&& expressionContext.expressionType == ExpressionType.OBJECT_INITIALIZER) {
return;
}
reportItems(reporter, collection);
if ((globalOptions & OPTION_GLOBALS) != 0) {
if (!requestor.isIgnored(CompletionProposal.TYPE_REF)) {
doCompletionOnType(TypeMode.CODE, reporter);
}
reportGlobals(reporter);
}
if ((globalOptions & OPTION_KEYWORDS) != 0) {
if (!requestor.isIgnored(CompletionProposal.KEYWORD)) {
doCompletionOnKeyword(reporter.getPrefix(),
reporter.getPosition(), expressionContext);
}
}
}
private void doCompletionOnKeyword(String prefix, int position,
ExpressionContext expressionContext) {
final String[] keywords;
if (expressionContext != null) {
if (expressionContext.expressionType == ExpressionType.PROPERTY_INITIALIZER_VALUE) {
keywords = JavaScriptKeywords.getJavaScriptValueKeywords();
} else {
keywords = JavaScriptKeywords.getJavaScriptExpressionKeywords();
}
} else {
keywords = JavaScriptKeywords.getJavaScriptKeywords();
}
findKeywords(prefix.toCharArray(), keywords, true);
}
}