diff --git a/src/com/google/javascript/jscomp/AbstractScope.java b/src/com/google/javascript/jscomp/AbstractScope.java index 903cd1a0d20..c4eb5a98996 100644 --- a/src/com/google/javascript/jscomp/AbstractScope.java +++ b/src/com/google/javascript/jscomp/AbstractScope.java @@ -389,7 +389,7 @@ private S thisScope() { } /** Performs simple validity checks on when constructing a child scope. */ - void checkChildScope(S parent) { + final void checkChildScope(S parent) { checkNotNull(parent); checkArgument(NodeUtil.createsScope(rootNode), rootNode); checkArgument( @@ -398,7 +398,7 @@ void checkChildScope(S parent) { } /** Performs simple validity checks on when constructing a root scope. */ - void checkRootScope() { + final void checkRootScope() { // TODO(tbreisacher): Can we tighten this to just NodeUtil.createsScope? checkArgument( NodeUtil.createsScope(rootNode) || rootNode.isScript() || rootNode.isRoot(), rootNode); diff --git a/src/com/google/javascript/jscomp/FunctionTypeBuilder.java b/src/com/google/javascript/jscomp/FunctionTypeBuilder.java index 11afa907252..f56c3b82276 100644 --- a/src/com/google/javascript/jscomp/FunctionTypeBuilder.java +++ b/src/com/google/javascript/jscomp/FunctionTypeBuilder.java @@ -39,6 +39,7 @@ import com.google.javascript.rhino.jstype.JSType; import com.google.javascript.rhino.jstype.JSTypeRegistry; import com.google.javascript.rhino.jstype.ObjectType; +import com.google.javascript.rhino.jstype.StaticTypedScope; import com.google.javascript.rhino.jstype.TemplateType; import java.util.ArrayList; import java.util.HashSet; @@ -71,7 +72,7 @@ final class FunctionTypeBuilder { private final CodingConvention codingConvention; private final JSTypeRegistry typeRegistry; private final Node errorRoot; - private final TypedScope scope; + private final TypedScope enclosingScope; private FunctionContents contents = UnknownFunctionContents.get(); @@ -94,6 +95,7 @@ final class FunctionTypeBuilder { // list. private ImmutableList classTemplateTypeNames = ImmutableList.of(); private TypedScope declarationScope = null; + private StaticTypedScope templateScope; static final DiagnosticType EXTENDS_WITHOUT_TYPEDEF = DiagnosticType.warning( "JSC_EXTENDS_WITHOUT_TYPEDEF", @@ -226,13 +228,13 @@ public boolean apply(JSType type) { FunctionTypeBuilder(String fnName, AbstractCompiler compiler, Node errorRoot, TypedScope scope) { checkNotNull(errorRoot); - this.fnName = nullToEmpty(fnName); this.codingConvention = compiler.getCodingConvention(); this.typeRegistry = compiler.getTypeRegistry(); this.errorRoot = errorRoot; this.compiler = compiler; - this.scope = scope; + this.enclosingScope = scope; + this.templateScope = scope; } /** Format the function name for use in warnings. */ @@ -343,7 +345,7 @@ FunctionTypeBuilder inferReturnType( JSTypeExpression returnTypeExpr = fromInlineDoc ? info.getType() : info.getReturnType(); if (returnTypeExpr != null) { - returnType = returnTypeExpr.evaluate(scope, typeRegistry); + returnType = returnTypeExpr.evaluate(templateScope, typeRegistry); returnTypeInferred = false; } } @@ -394,7 +396,6 @@ FunctionTypeBuilder inferInheritance( if (nativeClassTemplateTypeNames != null && infoTemplateTypeNames.size() == nativeClassTemplateTypeNames.size()) { classTemplateTypeNames = nativeClassTemplateTypeNames; - typeRegistry.setTemplateTypeNames(classTemplateTypeNames); } else if (!infoTemplateTypeNames.isEmpty() && (isConstructor || isInterface)) { // Otherwise, create new template type for // the template values of the constructor/interface @@ -405,15 +406,25 @@ FunctionTypeBuilder inferInheritance( builder.add(typeRegistry.createTemplateType(typeParameter)); } classTemplateTypeNames = builder.build(); - typeRegistry.setTemplateTypeNames(classTemplateTypeNames); + } + + if (!classTemplateTypeNames.isEmpty()) { + // Make a new templateScope for resolving types against. + templateScope = typeRegistry.createScopeWithTemplates(templateScope, classTemplateTypeNames); + + // Register the template types on the function node. + Node functionNode = contents != null ? contents.getSourceNode() : null; + if (functionNode != null) { + typeRegistry.registerTemplateTypeNamesInScope(classTemplateTypeNames, functionNode); + } } // base type if (info != null && info.hasBaseType()) { if (isConstructor) { ObjectType infoBaseType = - info.getBaseType().evaluate(scope, typeRegistry).toMaybeObjectType(); - // TODO(sdh): ensure that JSDoc's baseType and AST's baseType are compatible if both are set + info.getBaseType().evaluate(templateScope, typeRegistry).toMaybeObjectType(); + // TODO(sdh): ensure JSDoc's baseType and AST's baseType are compatible if both are set baseType = infoBaseType; } else { reportWarning(EXTENDS_WITHOUT_TYPEDEF, formatFnName()); @@ -431,7 +442,7 @@ FunctionTypeBuilder inferInheritance( implementedInterfaces = new ArrayList<>(); Set baseInterfaces = new HashSet<>(); for (JSTypeExpression t : info.getImplementedInterfaces()) { - JSType maybeInterType = t.evaluate(scope, typeRegistry); + JSType maybeInterType = t.evaluate(templateScope, typeRegistry); if (maybeInterType != null && maybeInterType.setValidator(new ImplementedTypeValidator())) { @@ -463,7 +474,7 @@ FunctionTypeBuilder inferInheritance( extendedInterfaces = new ArrayList<>(); if (info != null) { for (JSTypeExpression t : info.getExtendedInterfaces()) { - JSType maybeInterfaceType = t.evaluate(scope, typeRegistry); + JSType maybeInterfaceType = t.evaluate(templateScope, typeRegistry); if (maybeInterfaceType != null && maybeInterfaceType.setValidator(new ExtendedTypeValidator())) { extendedInterfaces.add((ObjectType) maybeInterfaceType); @@ -510,7 +521,7 @@ FunctionTypeBuilder inferThisType(JSDocInfo info) { // undefined "this" value, but all the existing "@this" annotations // don't declare restricted types. JSType maybeThisType = - info.getThisType().evaluate(scope, typeRegistry).restrictByNotNullOrUndefined(); + info.getThisType().evaluate(templateScope, typeRegistry).restrictByNotNullOrUndefined(); if (maybeThisType != null) { thisType = maybeThisType; } @@ -573,11 +584,10 @@ FunctionTypeBuilder inferParameterTypes(@Nullable Node argsParent, @Nullable JSD JSType parameterType = null; if (info != null && info.hasParameterType(argumentName)) { parameterType = - info.getParameterType(argumentName).evaluate(scope, typeRegistry); + info.getParameterType(argumentName).evaluate(templateScope, typeRegistry); } else if (arg.getJSDocInfo() != null && arg.getJSDocInfo().hasType()) { JSTypeExpression parameterTypeExpression = arg.getJSDocInfo().getType(); - parameterType = - parameterTypeExpression.evaluate(scope, typeRegistry); + parameterType = parameterTypeExpression.evaluate(templateScope, typeRegistry); isOptionalParam = parameterTypeExpression.isOptionalArg(); isVarArgs = parameterTypeExpression.isVarArgs(); } else if (oldParameterType != null && @@ -657,24 +667,17 @@ FunctionTypeBuilder inferTemplateTypeName(@Nullable JSDocInfo info, @Nullable JS // of inherited ones from an overridden function. if (info != null) { ImmutableList.Builder builder = ImmutableList.builder(); - ImmutableList infoTemplateTypeNames = - info.getTemplateTypeNames(); - ImmutableMap infoTypeTransformations = - info.getTypeTransformations(); - if (!infoTemplateTypeNames.isEmpty()) { - for (String key : infoTemplateTypeNames) { - builder.add(typeRegistry.createTemplateType(key)); - } + ImmutableList infoTemplateTypeNames = info.getTemplateTypeNames(); + ImmutableMap infoTypeTransformations = info.getTypeTransformations(); + for (String key : infoTemplateTypeNames) { + builder.add(typeRegistry.createTemplateType(key)); } - if (!infoTypeTransformations.isEmpty()) { - for (Entry entry : infoTypeTransformations.entrySet()) { - builder.add(typeRegistry.createTemplateTypeWithTransformation( - entry.getKey(), entry.getValue())); - } + for (Entry entry : infoTypeTransformations.entrySet()) { + builder.add(typeRegistry.createTemplateTypeWithTransformation( + entry.getKey(), entry.getValue())); } - if (!infoTemplateTypeNames.isEmpty() - || !infoTypeTransformations.isEmpty()) { - templateTypeNames = builder.build(); + if (!infoTemplateTypeNames.isEmpty() || !infoTypeTransformations.isEmpty()) { + this.templateTypeNames = builder.build(); } } @@ -684,6 +687,9 @@ FunctionTypeBuilder inferTemplateTypeName(@Nullable JSDocInfo info, @Nullable JS ownerType.getTemplateTypeMap().getTemplateKeys(); if (!ownerTypeKeys.isEmpty()) { ImmutableList.Builder builder = ImmutableList.builder(); + // TODO(sdh): The order of these should be switched to avoid class templates shadowing + // method templates, but this currently loosens type checking of arrays more than we'd like. + // See http://github.com/google/closure-compiler/issues/2973 builder.addAll(templateTypeNames); builder.addAll(ownerTypeKeys); keys = builder.build(); @@ -691,7 +697,14 @@ FunctionTypeBuilder inferTemplateTypeName(@Nullable JSDocInfo info, @Nullable JS } if (!keys.isEmpty()) { - typeRegistry.setTemplateTypeNames(keys); + // Add any templates from JSDoc into our template scope. + templateScope = typeRegistry.createScopeWithTemplates(templateScope, keys); + + // Register the template types on the function node. + Node functionNode = contents != null ? contents.getSourceNode() : null; + if (functionNode != null) { + typeRegistry.registerTemplateTypeNamesInScope(keys, functionNode); + } } return this; } @@ -817,8 +830,6 @@ FunctionType buildAndRegister() { fnType.setImplicitMatch(true); } - typeRegistry.clearTemplateTypeNames(); - return fnType; } @@ -965,12 +976,12 @@ private TypedScope getScopeDeclaredIn() { int dotIndex = fnName.indexOf('.'); if (dotIndex != -1) { String rootVarName = fnName.substring(0, dotIndex); - TypedVar rootVar = scope.getVar(rootVarName); + TypedVar rootVar = enclosingScope.getVar(rootVarName); if (rootVar != null) { return rootVar.getScope(); } } - return scope; + return enclosingScope; } /** diff --git a/src/com/google/javascript/jscomp/TypedScopeCreator.java b/src/com/google/javascript/jscomp/TypedScopeCreator.java index 2306ec6e684..1ff23a7f164 100644 --- a/src/com/google/javascript/jscomp/TypedScopeCreator.java +++ b/src/com/google/javascript/jscomp/TypedScopeCreator.java @@ -75,6 +75,7 @@ import com.google.javascript.rhino.jstype.NominalTypeBuilderOti; import com.google.javascript.rhino.jstype.ObjectType; import com.google.javascript.rhino.jstype.Property; +import com.google.javascript.rhino.jstype.StaticTypedScope; import com.google.javascript.rhino.jstype.TemplateType; import com.google.javascript.rhino.jstype.TemplateTypeMap; import com.google.javascript.rhino.jstype.TemplateTypeMapReplacer; @@ -540,6 +541,11 @@ private abstract class AbstractScopeBuilder implements NodeTraversal.Callback { this.currentHoistScope = scope.getClosestHoistScope(); } + /** Returns the current compiler input. */ + CompilerInput getCompilerInput() { + return compiler.getInput(inputId); + } + /** Traverse the scope root and build it. */ void build() { NodeTraversal.traverse(compiler, currentScope.getRootNode(), this); @@ -770,15 +776,11 @@ private JSType getDeclaredTypeInAnnotation(Node node, JSDocInfo info) { } } - if (!ownerTypeKeys.isEmpty()) { - typeRegistry.setTemplateTypeNames(ownerTypeKeys); - } - - jsType = info.getType().evaluate(currentScope, typeRegistry); - - if (!ownerTypeKeys.isEmpty()) { - typeRegistry.clearTemplateTypeNames(); - } + StaticTypedScope templateScope = + !ownerTypeKeys.isEmpty() + ? typeRegistry.createScopeWithTemplates(currentScope, ownerTypeKeys) + : currentScope; + jsType = info.getType().evaluate(templateScope, typeRegistry); } else if (FunctionTypeBuilder.isFunctionTypeDeclaration(info)) { String fnName = node.getQualifiedName(); jsType = createFunctionTypeFromNodes(null, fnName, info, node); @@ -2394,6 +2396,34 @@ void declareArguments() { } } } + // Also add template params to the scope so that JSTypeRegistry can find them (they + // were already registered by FunctionTypeBuilder). + JSDocInfo info = NodeUtil.getBestJSDocInfo(functionNode); + if (info != null) { + Iterable templateNames = + Iterables.concat(info.getTemplateTypeNames(), info.getTypeTransformations().keySet()); + if (!Iterables.isEmpty(templateNames)) { + CompilerInput input = getCompilerInput(); + JSType voidType = typeRegistry.getNativeType(VOID_TYPE); + // Declare any template names in the function scope. This means that if someone shadows + // an outer variable FOO with a @template FOO and refers to FOO inside the method, we + // will treat it as undefined, rather than the correct type, which could lead to weird + // errors. Ideally we'd have a "don't use me" type that gives an error at use. + for (String name : templateNames) { + if (!currentScope.canDeclare(name)) { + validator.expectUndeclaredVariable( + NodeUtil.getSourceName(functionNode), + input, + functionNode, + functionNode.getParent(), + currentScope.getVar(name), + name, + voidType); + } + currentScope.declare(name, functionNode, voidType, input, /* inferred= */ false); + } + } + } } } // end declareArguments } // end FunctionScopeBuilder diff --git a/src/com/google/javascript/rhino/HamtPMap.java b/src/com/google/javascript/rhino/HamtPMap.java index b030573bfbe..9c1aeba864c 100644 --- a/src/com/google/javascript/rhino/HamtPMap.java +++ b/src/com/google/javascript/rhino/HamtPMap.java @@ -42,6 +42,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; +import java.io.Serializable; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collections; @@ -66,7 +67,7 @@ */ // TODO(sdh): Consider using tricks from https://bendyworks.com/blog/leveling-clojures-hash-maps // We need a solid way to profile the results to see if it's actually worth the extra code. -public final class HamtPMap implements PMap { +public final class HamtPMap implements PMap, Serializable { /** * Number of bits of fan-out at each level. May be anywhere from 1 (a binary tree) to 5 (for a diff --git a/src/com/google/javascript/rhino/jstype/JSTypeRegistry.java b/src/com/google/javascript/rhino/jstype/JSTypeRegistry.java index f78d71f74de..ae25c7cf8c8 100644 --- a/src/com/google/javascript/rhino/jstype/JSTypeRegistry.java +++ b/src/com/google/javascript/rhino/jstype/JSTypeRegistry.java @@ -58,10 +58,12 @@ import com.google.common.collect.Multimap; import com.google.common.collect.Table; import com.google.javascript.rhino.ErrorReporter; +import com.google.javascript.rhino.HamtPMap; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.JSTypeExpression; import com.google.javascript.rhino.Node; +import com.google.javascript.rhino.PMap; import com.google.javascript.rhino.SimpleErrorReporter; import com.google.javascript.rhino.StaticScope; import com.google.javascript.rhino.StaticSlot; @@ -210,9 +212,6 @@ public class JSTypeRegistry implements Serializable { // All the unresolved named types. private final List unresolvedNamedTypes = new ArrayList<>(); - // The template type name. - private final Map templateTypes = new HashMap<>(); - // A single empty TemplateTypeMap, which can be safely reused in cases where // there are no template types. private final TemplateTypeMap emptyTemplateTypeMap; @@ -769,6 +768,12 @@ private static void checkTypeName(String typeName) { private JSType getTypeInternal(StaticScope scope, String name) { checkTypeName(name); + if (scope instanceof SyntheticTemplateScope) { + TemplateType type = ((SyntheticTemplateScope) scope).getTemplateType(name); + if (type != null) { + return type; + } + } return getTypeForScopeInternal(getLookupScope(scope, name), name); } @@ -1262,11 +1267,6 @@ private JSType getJSTypeOrUnknown(Node n) { * @return the corresponding JSType object or {@code null} it cannot be found */ public JSType getTypeForScope(StaticScope scope, String jsTypeName) { - TemplateType templateType = templateTypes.get(jsTypeName); - if (templateType != null) { - return templateType; - } - return getTypeForScopeInternal(scope, jsTypeName); } @@ -1281,11 +1281,6 @@ public JSType getGlobalType(String jsTypeName) { * @return the corresponding JSType object or {@code null} it cannot be found */ public JSType getType(StaticScope scope, String jsTypeName) { - TemplateType templateType = templateTypes.get(jsTypeName); - if (templateType != null) { - return templateType; - } - return getTypeInternal(scope, jsTypeName); } @@ -2159,20 +2154,76 @@ private JSType createRecordTypeFromNodes(Node n, String sourceName, StaticTypedS } /** - * Sets the template type name. + * Registers template types on the given scope root. This takes a Node rather than a + * StaticScope because at the time it is called, the scope has not yet been created. */ - public void setTemplateTypeNames(List keys) { - checkNotNull(keys); + public void registerTemplateTypeNamesInScope(List keys, Node scopeRoot) { for (TemplateType key : keys) { - templateTypes.put(key.getReferenceName(), key); + scopedNameTable.put(scopeRoot, key.getReferenceName(), key); } } /** - * Clears the template type name. + * Returns a new scope that includes the given template names for type resolution + * purposes. */ - public void clearTemplateTypeNames() { - templateTypes.clear(); + public StaticTypedScope createScopeWithTemplates( + StaticTypedScope scope, Iterable templates) { + return new SyntheticTemplateScope(scope, templates); + } + + /** + * Synthetic scope that includes template names. This is necessary for resolving + * template names outside the body of templated functions (e.g. when evaluating + * JSDoc on things assigned to a prototype, or the parameter or return types of + * an annotated function), since there is not yet (and may never be) any real + * scope to attach the types to. + */ + private static class SyntheticTemplateScope implements StaticTypedScope, Serializable { + final StaticTypedScope delegate; + final PMap types; + + SyntheticTemplateScope(StaticTypedScope delegate, Iterable templates) { + this.delegate = delegate; + PMap types = + delegate instanceof SyntheticTemplateScope + ? ((SyntheticTemplateScope) delegate).types + : HamtPMap.empty(); + for (TemplateType key : templates) { + types = types.plus(key.getReferenceName(), key); + } + this.types = types; + } + + @Override + public Node getRootNode() { + return delegate.getRootNode(); + } + + @Override + public StaticTypedScope getParentScope() { + return delegate.getParentScope(); + } + + @Override + public StaticTypedSlot getSlot(String name) { + return delegate.getSlot(name); + } + + @Override + public StaticTypedSlot getOwnSlot(String name) { + return delegate.getOwnSlot(name); + } + + @Override + public JSType getTypeOfThis() { + return delegate.getTypeOfThis(); + } + + @Nullable + TemplateType getTemplateType(String name) { + return types.get(name); + } } /** diff --git a/test/com/google/javascript/jscomp/TypeCheckTest.java b/test/com/google/javascript/jscomp/TypeCheckTest.java index b652c9898bf..3eb21ae305d 100644 --- a/test/com/google/javascript/jscomp/TypeCheckTest.java +++ b/test/com/google/javascript/jscomp/TypeCheckTest.java @@ -14144,7 +14144,21 @@ public void testTemplateType26() { "initializing variable\n" + "found : Array\n" + "required: null"); } - public void testTemplateTypeForwardReferenceFunction() { + public void testTemplateTypeCollidesWithParameter() { + // Function templates are in the same scope as parameters, so cannot collide. + testTypes( + lines( + "/**", // + " * @param {T} T", + " * @template T", + " */", + "function f(T) {}"), + "variable T redefined with type undefined, " + + "original definition at [testcode]:5 with type T"); + } + + public void testTemplateTypeForwardReference() { + // TODO(martinprobst): the test below asserts incorrect behavior for backwards compatibility. testTypes( lines( "/** @param {!Foo} x */", @@ -20313,6 +20327,120 @@ public void testQuotedPropertyWithDotInCode() { }); } + public void testClassTemplateParamsInMethodBody() { + testTypes( + lines( + "/** @constructor @template T */", + "function Foo() {}", + "Foo.prototype.foo = function() {", + " var /** T */ x;", + "};")); + } + + public void testClassTtlParamsInMethodBody() { + // NOTE: TTL is not supported in class templates, so this does not work. + // This test simply documents this fact. + testTypes( + lines( + "/** @constructor @template T := 'number' =: */", + "function Foo() {}", + "Foo.prototype.foo = function() {", + " var /** T */ x;", + "};"), + "Bad type annotation. Unknown type T"); + } + + public void testClassTtlParamsInMethodSignature() { + // NOTE: TTL is not supported in class templates, so this does not work. + // This test simply documents this fact. + testTypes( + lines( + "/** @constructor @template T := 'number' =: */", + "function Foo() {}", + "/** @return {T} */", + "Foo.prototype.foo = function() {};"), + "Bad type annotation. Unknown type T"); + } + + public void testFunctionTemplateParamsInBody() { + testTypes( + lines( + "/** @template T */", + "function f(/** !Array */ arr) {", + " var /** T */ first = arr[0];", + "}")); + } + + public void testFunctionTtlParamsInBody() { + testTypes( + lines( + "/** @template T := 'number' =: */", + "function f(/** !Array */ arr) {", + " var /** T */ first = arr[0];", + "}")); + } + + public void testFunctionTemplateParamsInBodyMismatch() { + testTypes( + lines( + "/** @template T, V */", + "function f(/** !Array */ ts, /** !Array */ vs) {", + " var /** T */ t = vs[0];", + "}")); + // TODO(b/35241823): This should be an error, but we currently treat generic types + // as unknown. Once we have bounded generics we can treat templates as unique types. + // lines( + // "actual parameter 1 of f does not match formal parameter", + // "found : V", + // "required: T")); + } + + public void testClassAndMethodTemplateParamsInMethodBody() { + testTypes( + lines( + "/** @constructor @template T */", + "function Foo() {}", + "/** @template U */", + "Foo.prototype.foo = function() {", + " var /** T */ x;", + " var /** U */ y;", + "};")); + } + + public void testNestedFunctionTemplates() { + testTypes( + lines( + "/** @constructor @template A, B */", + "function Foo(/** A */ a, /** B */ b) {}", + "/** @template T */", + "function f(/** T */ t) {", + " /** @template S */", + " function g(/** S */ s) {", + " var /** !Foo */ foo = new Foo(t, s);", + " }", + "}")); + } + + public void testNestedFunctionTemplatesMismatch() { + testTypes( + lines( + "/** @constructor @template A, B */", + "function Foo(/** A */ a, /** B */ b) {}", + "/** @template T */", + "function f(/** T */ t) {", + " /** @template S */", + " function g(/** S */ s) {", + " var /** !Foo */ foo = new Foo(s, t);", + " }", + "}")); + // TODO(b/35241823): This should be an error, but we currently treat generic types + // as unknown. Once we have bounded generics we can treat templates as unique types. + // lines( + // "initializing variable", + // "found : Foo", + // "required: Foo")); + } + private void testClosureTypes(String js, String description) { testClosureTypesMultipleWarnings(js, description == null ? null : ImmutableList.of(description)); diff --git a/test/com/google/javascript/jscomp/TypeCheckTestCase.java b/test/com/google/javascript/jscomp/TypeCheckTestCase.java index 3eebfba5fbb..799882f524c 100644 --- a/test/com/google/javascript/jscomp/TypeCheckTestCase.java +++ b/test/com/google/javascript/jscomp/TypeCheckTestCase.java @@ -262,7 +262,6 @@ protected TypeCheckResult parseAndTypeCheckWithScope(String js) { protected TypeCheckResult parseAndTypeCheckWithScope(String externs, String js) { registry.clearNamedTypes(); - registry.clearTemplateTypeNames(); compiler.init( ImmutableList.of(SourceFile.fromCode("[externs]", externs)), ImmutableList.of(SourceFile.fromCode("[testcode]", js)),