/* * Copyright 2010-2015 JetBrains s.r.o. * * Licensed 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.jetbrains.kotlin.js.test.utils; import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.js.backend.ast.*; import org.jetbrains.kotlin.js.inline.util.CollectUtilsKt; import org.jetbrains.kotlin.js.translate.expression.InlineMetadata; import java.util.*; import static org.jetbrains.kotlin.js.inline.util.CollectUtilsKt.collectInstances; import static org.jetbrains.kotlin.test.InTextDirectivesUtils.findLinesWithPrefixesRemoved; import static org.junit.Assert.*; public class DirectiveTestUtils { private DirectiveTestUtils() {} private static final DirectiveHandler FUNCTION_CONTAINS_NO_CALLS = new DirectiveHandler("CHECK_CONTAINS_NO_CALLS") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { Set<String> exceptNames = new HashSet<>(); String exceptNamesArg = arguments.findNamedArgument("except"); if (exceptNamesArg != null) { for (String exceptName : exceptNamesArg.split(";")) { exceptNames.add(exceptName.trim()); } } checkFunctionContainsNoCalls(ast, arguments.getFirst(), exceptNames); } }; private static final DirectiveHandler FUNCTION_NOT_CALLED = new DirectiveHandler("CHECK_NOT_CALLED") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { checkFunctionNotCalled(ast, arguments.getFirst(), arguments.findNamedArgument("except")); } }; private static final DirectiveHandler PROPERTY_NOT_USED = new DirectiveHandler("PROPERTY_NOT_USED") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { checkPropertyNotUsed(ast, arguments.getFirst(), false, false); } }; private static final DirectiveHandler PROPERTY_NOT_READ_FROM = new DirectiveHandler("PROPERTY_NOT_READ_FROM") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { checkPropertyNotUsed(ast, arguments.getFirst(), false, true); } }; private static final DirectiveHandler PROPERTY_NOT_WRITTEN_TO = new DirectiveHandler("PROPERTY_NOT_WRITTEN_TO") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { checkPropertyNotUsed(ast, arguments.getFirst(), true, false); } }; private static final DirectiveHandler PROPERTY_WRITE_COUNT = new DirectiveHandler("PROPERTY_WRITE_COUNT") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { checkPropertyWriteCount(ast, arguments.getNamedArgument("name"), Integer.parseInt(arguments.getNamedArgument("count"))); } }; private static final DirectiveHandler FUNCTION_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_CALLED_IN_SCOPE") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { // Be more restrictive, check qualified match by default checkCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope"), parseBooleanArgument(arguments, "qualified", true)); } }; private static final DirectiveHandler FUNCTION_NOT_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_NOT_CALLED_IN_SCOPE") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { // Be more restrictive, check unqualified match by default checkNotCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope"), parseBooleanArgument(arguments, "qualified", false)); } }; private static final DirectiveHandler FUNCTION_CALLED_TIMES = new DirectiveHandler("FUNCTION_CALLED_TIMES") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { int expectedCount = Integer.parseInt(arguments.getNamedArgument("count")); String functionName = arguments.getFirst(); CallCounter counter = CallCounter.countCalls(ast); int actualCount = counter.getUnqualifiedCallsCount(functionName); assertEquals("Function " + functionName, expectedCount, actualCount); } }; private static boolean parseBooleanArgument(@NotNull ArgumentsHelper arguments, @NotNull String name, boolean defaultValue) { String value = arguments.findNamedArgument(name); return value != null ? Boolean.parseBoolean(value) : defaultValue; } private static final DirectiveHandler FUNCTIONS_HAVE_SAME_LINES = new DirectiveHandler("CHECK_FUNCTIONS_HAVE_SAME_LINES") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { String code1 = getFunctionCode(ast, arguments.getPositionalArgument(0)); String code2 = getFunctionCode(ast, arguments.getPositionalArgument(1)); String regexMatch = arguments.findNamedArgument("match"); String regexReplace = arguments.findNamedArgument("replace"); code1 = applyRegex(code1, regexMatch, regexReplace); code2 = applyRegex(code2, regexMatch, regexReplace); assertEquals(code1, code2); } @NotNull String getFunctionCode(@NotNull JsNode ast, @NotNull String functionName) { JsFunction function = AstSearchUtil.getFunction(ast, functionName); return function.getBody().toString(); } @NotNull String applyRegex(@NotNull String code, @Nullable String match, @Nullable String replace) { if (match == null || replace == null) return code; return code.replaceAll(match, replace); } }; private static class CountNodesDirective<T extends JsNode> extends DirectiveHandler { @NotNull private final Class<T> klass; CountNodesDirective(@NotNull String directive, @NotNull Class<T> klass) { super(directive); this.klass = klass; } @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { String functionName = arguments.getNamedArgument("function"); String countStr = arguments.getNamedArgument("count"); int expectedCount = Integer.valueOf(countStr); JsFunction function = AstSearchUtil.getFunction(ast, functionName); List<T> nodes = collectInstances(klass, function.getBody()); int actualCount = 0; for (T node : nodes) { actualCount += getActualCountFor(node, arguments); } String message = "Function " + functionName + " contains " + actualCount + " nodes of type " + klass.getName() + ", but expected count is " + expectedCount; assertEquals(message, expectedCount, actualCount); } protected int getActualCountFor(@NotNull T node, @NotNull ArgumentsHelper arguments) { return 1; } } private static final DirectiveHandler COUNT_LABELS = new CountNodesDirective<JsLabel>("CHECK_LABELS_COUNT", JsLabel.class) { @Override protected int getActualCountFor(@NotNull JsLabel node, @NotNull ArgumentsHelper arguments) { String labelName = arguments.findNamedArgument("name"); if (labelName == null) { return 1; } return node.getName().getIdent().equals(labelName) ? 1 : 0; } }; private static final DirectiveHandler COUNT_VARS = new CountNodesDirective<>("CHECK_VARS_COUNT", JsVars.JsVar.class); private static final DirectiveHandler COUNT_BREAKS = new CountNodesDirective<>("CHECK_BREAKS_COUNT", JsBreak.class); private static final DirectiveHandler COUNT_NULLS = new CountNodesDirective<>("CHECK_NULLS_COUNT", JsNullLiteral.class); private static final DirectiveHandler NOT_REFERENCED = new DirectiveHandler("CHECK_NOT_REFERENCED") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { String reference = arguments.getPositionalArgument(0); JsVisitor visitor = new RecursiveJsVisitor() { @Override public void visitNameRef(@NotNull JsNameRef nameRef) { assertNotEquals(reference, nameRef.toString()); } }; visitor.accept(ast); } }; private static final DirectiveHandler ONLY_THIS_QUALIFIED_REFERENCES = new DirectiveHandler("ONLY_THIS_QUALIFIED_REFERENCES") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { String fieldName = arguments.getPositionalArgument(0); QualifiedReferenceCollector collector = new QualifiedReferenceCollector(fieldName); ast.accept(collector); assertTrue("No reference to field '" + fieldName + "' found", collector.hasReferences); assertTrue("There are references to field '" + fieldName + "' not qualified by 'this' literal", collector.allReferencesQualifiedByThis); } }; static class QualifiedReferenceCollector extends RecursiveJsVisitor { private final String nameToSearch; boolean hasReferences; boolean allReferencesQualifiedByThis = true; public QualifiedReferenceCollector(String nameToSearch) { this.nameToSearch = nameToSearch; } @Override public void visitNameRef(@NotNull JsNameRef nameRef) { super.visitNameRef(nameRef); JsName name = nameRef.getName(); if (name == null) return; if (name.getIdent().equals(nameToSearch)) { hasReferences = true; if (!(nameRef.getQualifier() instanceof JsLiteral.JsThisRef)) { allReferencesQualifiedByThis = false; } } } } private static final DirectiveHandler HAS_INLINE_METADATA = new DirectiveHandler("CHECK_HAS_INLINE_METADATA") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { String functionName = arguments.getPositionalArgument(0); JsExpression property = AstSearchUtil.getMetadataOrFunction(ast, functionName); String message = "Inline metadata has not been generated for function " + functionName; assertNotNull(message, InlineMetadata.decompose(property)); } }; private static final DirectiveHandler HAS_NO_INLINE_METADATA = new DirectiveHandler("CHECK_HAS_NO_INLINE_METADATA") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { String functionName = arguments.getPositionalArgument(0); JsExpression property = AstSearchUtil.getMetadataOrFunction(ast, functionName); String message = "Inline metadata has been generated for not effectively public function " + functionName; assertTrue(message, property instanceof JsFunction); } }; private static final DirectiveHandler HAS_NO_CAPTURED_VARS = new DirectiveHandler("HAS_NO_CAPTURED_VARS") { @Override void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception { String functionName = arguments.getNamedArgument("function"); Set<String> except = new HashSet<>(); String exceptString = arguments.findNamedArgument("except"); if (exceptString != null) { for (String exceptId : StringUtil.split(exceptString, ";")) { except.add(exceptId.trim()); } } JsFunction function = AstSearchUtil.getFunction(ast, functionName); Set<JsName> freeVars = CollectUtilsKt.collectFreeVariables(function); for (JsName freeVar : freeVars) { assertTrue("Function " + functionName + " captures free variable " + freeVar.getIdent(), except.contains(freeVar.getIdent())); } } }; private static final List<DirectiveHandler> DIRECTIVE_HANDLERS = Arrays.asList( FUNCTION_CONTAINS_NO_CALLS, FUNCTION_NOT_CALLED, FUNCTION_CALLED_TIMES, PROPERTY_NOT_USED, PROPERTY_NOT_READ_FROM, PROPERTY_NOT_WRITTEN_TO, PROPERTY_WRITE_COUNT, FUNCTION_CALLED_IN_SCOPE, FUNCTION_NOT_CALLED_IN_SCOPE, FUNCTIONS_HAVE_SAME_LINES, ONLY_THIS_QUALIFIED_REFERENCES, COUNT_LABELS, COUNT_VARS, COUNT_BREAKS, COUNT_NULLS, NOT_REFERENCED, HAS_INLINE_METADATA, HAS_NO_INLINE_METADATA, HAS_NO_CAPTURED_VARS ); public static void processDirectives(@NotNull JsNode ast, @NotNull String sourceCode) throws Exception { for (DirectiveHandler handler : DIRECTIVE_HANDLERS) { handler.process(ast, sourceCode); } } public static void checkFunctionContainsNoCalls(JsNode node, String functionName, @NotNull Set<String> exceptFunctionNames) throws Exception { JsFunction function = AstSearchUtil.getFunction(node, functionName); CallCounter counter = CallCounter.countCalls(function, exceptFunctionNames); int callsCount = counter.getTotalCallsCount(); String errorMessage = functionName + " contains calls"; assertEquals(errorMessage, 0, callsCount); } public static void checkPropertyNotUsed(JsNode node, String propertyName, boolean isGetAllowed, boolean isSetAllowed) throws Exception { PropertyReferenceCollector counter = PropertyReferenceCollector.Companion.collect(node); if (!isGetAllowed) { assertFalse("inline property getter for `" + propertyName + "` is called", counter.hasUnqualifiedReads(propertyName)); } if (!isSetAllowed) { assertFalse("inline property setter for `" + propertyName + "` is called", counter.hasUnqualifiedWrites(propertyName)); } } private static void checkPropertyWriteCount(JsNode node, String propertyName, int expectedCount) throws Exception { PropertyReferenceCollector counter = PropertyReferenceCollector.Companion.collect(node); assertEquals("Property write count: " + propertyName, expectedCount, counter.unqualifiedWriteCount(propertyName)); } public static void checkFunctionNotCalled(@NotNull JsNode node, @NotNull String functionName, @Nullable String exceptFunction) throws Exception { Set<String> excludedScopes = exceptFunction != null ? Collections.singleton(exceptFunction) : Collections.emptySet(); CallCounter counter = CallCounter.countCallsWithExcludedScopes(node, excludedScopes); int functionCalledCount = counter.getQualifiedCallsCount(functionName); String errorMessage = "inline function `" + functionName + "` is called"; assertEquals(errorMessage, 0, functionCalledCount); assertEquals("Not all excluded scopes found", excludedScopes.size(), counter.getExcludedScopeOccurrenceCount()); } public static void checkCalledInScope( @NotNull JsNode node, @NotNull String functionName, @NotNull String scopeFunctionName, boolean checkQualifier ) throws Exception { String errorMessage = functionName + " is not called inside " + scopeFunctionName; assertFalse(errorMessage, isCalledInScope(node, functionName, scopeFunctionName, checkQualifier)); } public static void checkNotCalledInScope( @NotNull JsNode node, @NotNull String functionName, @NotNull String scopeFunctionName, boolean checkQualifier ) throws Exception { String errorMessage = functionName + " is called inside " + scopeFunctionName; assertTrue(errorMessage, isCalledInScope(node, functionName, scopeFunctionName, checkQualifier)); } private static boolean isCalledInScope( @NotNull JsNode node, @NotNull String functionName, @NotNull String scopeFunctionName, boolean checkQualifier ) throws Exception { JsNode scope = AstSearchUtil.getFunction(node, scopeFunctionName); CallCounter counter = CallCounter.countCalls(scope); if (checkQualifier) { return counter.getQualifiedCallsCount(functionName) == 0; } else { return counter.getUnqualifiedCallsCount(functionName) == 0; } } private abstract static class DirectiveHandler { @NotNull private final String directive; DirectiveHandler(@NotNull String directive) { this.directive = "// " + directive + ": "; } /** * Processes directive entries. * * Each entry is expected to have the following format: * `// DIRECTIVE: arguments * * @see ArgumentsHelper for arguments format */ void process(@NotNull JsNode ast, @NotNull String sourceCode) throws Exception { List<String> directiveEntries = findLinesWithPrefixesRemoved(sourceCode, directive); for (String directiveEntry : directiveEntries) { processEntry(ast, new ArgumentsHelper(directiveEntry)); } } abstract void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception; @NotNull String getName() { return directive; } } /** * Arguments format: ((namedArg|positionalArg)\s+)*` * * Where: namedArg -- "key=value" * positionalArg -- "value" * * Neither key, nor value should contain spaces. */ private static class ArgumentsHelper { private final List<String> positionalArguments = new ArrayList<>(); private final Map<String, String> namedArguments = new HashMap<>(); private final String entry; ArgumentsHelper(@NotNull String directiveEntry) { entry = directiveEntry; for (String argument: directiveEntry.split("\\s+")) { String[] keyVal = argument.split("="); switch (keyVal.length) { case 1: positionalArguments.add(keyVal[0]); break; case 2: namedArguments.put(keyVal[0], keyVal[1]); break; default: throw new AssertionError("Wrong argument format: " + argument); } } } @NotNull String getFirst() { return getPositionalArgument(0); } @NotNull String getPositionalArgument(int index) { assert positionalArguments.size() > index: "Argument at index `" + index + "` not found in entry: " + entry; return positionalArguments.get(index); } @NotNull String getNamedArgument(@NotNull String name) { assert namedArguments.containsKey(name): "Argument `" + name + "` not found in entry: " + entry; return namedArguments.get(name); } @Nullable String findNamedArgument(@NotNull String name) { return namedArguments.get(name); } } }