diff --git a/.idea/artifacts/KotlinPlugin.xml b/.idea/artifacts/KotlinPlugin.xml index db1665a520c..b5adfe25356 100644 --- a/.idea/artifacts/KotlinPlugin.xml +++ b/.idea/artifacts/KotlinPlugin.xml @@ -21,6 +21,7 @@ + diff --git a/js/js.frontend/js.frontend.iml b/js/js.frontend/js.frontend.iml index 887bd1954ef..0d9bc35f0f2 100644 --- a/js/js.frontend/js.frontend.iml +++ b/js/js.frontend/js.frontend.iml @@ -10,6 +10,7 @@ + \ No newline at end of file diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/KotlinJsCheckerProvider.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/KotlinJsCheckerProvider.kt index 75f7415a78d..4d0e835344b 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/KotlinJsCheckerProvider.kt +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/KotlinJsCheckerProvider.kt @@ -31,11 +31,11 @@ import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.js.translate.utils.AnnotationsUtils import org.jetbrains.kotlin.descriptors.Visibilities import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf -import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker +import org.jetbrains.kotlin.resolve.diagnostics.JsCallChecker public object KotlinJsCheckerProvider : AdditionalCheckerProvider( annotationCheckers = listOf(NativeInvokeChecker(), NativeGetterChecker(), NativeSetterChecker()), - additionalCallCheckers = listOf(), + additionalCallCheckers = listOf(JsCallChecker()), additionalTypeCheckers = listOf() ) diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java index f587eade68e..fb780d8ee09 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java @@ -17,13 +17,11 @@ package org.jetbrains.kotlin.js.resolve.diagnostics; import com.intellij.openapi.util.TextRange; -import org.jetbrains.kotlin.diagnostics.DiagnosticFactory0; -import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1; -import org.jetbrains.kotlin.diagnostics.DiagnosticFactory2; -import org.jetbrains.kotlin.diagnostics.Errors; +import org.jetbrains.kotlin.diagnostics.*; import org.jetbrains.kotlin.psi.JetDeclaration; import org.jetbrains.kotlin.psi.JetElement; import org.jetbrains.kotlin.psi.JetExpression; +import org.jetbrains.kotlin.resolve.diagnostics.JsCallData; import org.jetbrains.kotlin.types.JetType; import java.util.List; @@ -39,9 +37,9 @@ public interface ErrorsJs { DiagnosticFactory0 NATIVE_GETTER_RETURN_TYPE_SHOULD_BE_NULLABLE = DiagnosticFactory0.create(ERROR, DECLARATION_RETURN_TYPE); DiagnosticFactory0 NATIVE_SETTER_WRONG_RETURN_TYPE = DiagnosticFactory0.create(ERROR, DECLARATION_RETURN_TYPE); DiagnosticFactory2 NATIVE_INDEXER_WRONG_PARAMETER_COUNT = DiagnosticFactory2.create(ERROR, DECLARATION_SIGNATURE_OR_DEFAULT); - DiagnosticFactory2> JSCODE_ERROR = DiagnosticFactory2.create(ERROR, JsCodePositioningStrategy.INSTANCE$); - DiagnosticFactory2> JSCODE_WARNING = DiagnosticFactory2.create(WARNING, JsCodePositioningStrategy.INSTANCE$); - DiagnosticFactory0 JSCODE_ARGUMENT_SHOULD_BE_LITERAL = DiagnosticFactory0.create(ERROR, CALL_EXPRESSION); + DiagnosticFactory1 JSCODE_ERROR = DiagnosticFactory1.create(ERROR, JsCodePositioningStrategy.INSTANCE$); + DiagnosticFactory1 JSCODE_WARNING = DiagnosticFactory1.create(WARNING, JsCodePositioningStrategy.INSTANCE$); + DiagnosticFactory0 JSCODE_ARGUMENT_SHOULD_BE_CONSTANT = DiagnosticFactory0.create(ERROR, DEFAULT); @SuppressWarnings("UnusedDeclaration") Object _initializer = new Object() { diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt new file mode 100644 index 00000000000..07a4264aa0e --- /dev/null +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCallChecker.kt @@ -0,0 +1,182 @@ +/* + * 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.resolve.diagnostics + +import com.google.gwt.dev.js.AbortParsingException +import com.google.gwt.dev.js.rhino.* +import com.google.gwt.dev.js.rhino.Utils.* +import org.jetbrains.annotations.TestOnly +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.descriptors.CallableDescriptor +import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor +import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1 +import org.jetbrains.kotlin.diagnostics.DiagnosticSink +import org.jetbrains.kotlin.diagnostics.ParametrizedDiagnostic +import org.jetbrains.kotlin.js.patterns.DescriptorPredicate +import org.jetbrains.kotlin.js.patterns.PatternBuilder +import org.jetbrains.kotlin.js.resolve.diagnostics.ErrorsJs +import org.jetbrains.kotlin.psi.JetCallExpression +import org.jetbrains.kotlin.psi.JetExpression +import org.jetbrains.kotlin.psi.JetLiteralStringTemplateEntry +import org.jetbrains.kotlin.psi.JetStringTemplateExpression +import org.jetbrains.kotlin.renderer.Renderer +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker +import org.jetbrains.kotlin.resolve.calls.context.BasicCallResolutionContext +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator +import org.jetbrains.kotlin.types.JetType + +import com.intellij.openapi.util.TextRange +import java.io.StringReader + +import kotlin.platform.platformStatic +import org.jetbrains.kotlin.resolve.TemporaryBindingTrace + +public class JsCallChecker : CallChecker { + + class object { + private val JS_PATTERN: DescriptorPredicate = PatternBuilder.pattern("kotlin.js.js(String)") + + platformStatic + public fun ResolvedCall.isJsCall(): Boolean { + val descriptor = getResultingDescriptor() + return descriptor is SimpleFunctionDescriptor && JS_PATTERN.apply(descriptor) + } + } + + override fun check(resolvedCall: ResolvedCall, context: BasicCallResolutionContext) { + if (context.isAnnotationContext || !resolvedCall.isJsCall()) return + + val expression = resolvedCall.getCall().getCallElement() + if (expression !is JetCallExpression) return + + val arguments = expression.getValueArgumentList()?.getArguments() + val argument = arguments?.firstOrNull()?.getArgumentExpression() + + if (argument == null) return + + val stringType = KotlinBuiltIns.getInstance().getStringType() + val trace = TemporaryBindingTrace.create(context.trace, "JsCallChecker") + val evaluationResult = ConstantExpressionEvaluator.evaluate(argument, trace, stringType) + + if (evaluationResult == null) { + context.trace.report(ErrorsJs.JSCODE_ARGUMENT_SHOULD_BE_CONSTANT.on(argument)) + return + } + + val code = evaluationResult.getValue() as String + val reader = StringReader(code) + val errorReporter = JsCodeErrorReporter(argument, code, context.trace) + Context.enter().setErrorReporter(errorReporter) + + try { + val ts = TokenStream(reader, "js", 0) + val parser = Parser(IRFactory(ts), /* insideFunction = */ true) + parser.parse(ts) + } catch (e: AbortParsingException) { + // ignore + } finally { + Context.exit() + } + } +} + +class JsCodeErrorReporter( + private val nodeToReport: JetExpression, + private val code: String, + private val trace: BindingTrace +) : ErrorReporter { + override fun warning(message: String, startPosition: CodePosition, endPosition: CodePosition) { + report(ErrorsJs.JSCODE_WARNING, message, startPosition, endPosition) + } + + override fun error(message: String, startPosition: CodePosition, endPosition: CodePosition) { + report(ErrorsJs.JSCODE_ERROR, message, startPosition, endPosition) + throw AbortParsingException() + } + + private fun report( + diagnosticFactory: DiagnosticFactory1, + message: String, + startPosition: CodePosition, + endPosition: CodePosition + ) { + val data = when { + nodeToReport.isConstantStringLiteral -> { + val reportRange = TextRange(startPosition.absoluteOffset, endPosition.absoluteOffset) + JsCallData(reportRange, message) + } + else -> { + val reportRange = nodeToReport.getTextRange() + val codeRange = TextRange(code.offsetOf(startPosition), code.offsetOf(endPosition)) + JsCallDataWithCode(reportRange, message, code, codeRange) + } + } + + val parametrizedDiagnostic = diagnosticFactory.on(nodeToReport, data) + trace.report(parametrizedDiagnostic) + } + + private val CodePosition.absoluteOffset: Int + get() { + val quotesLength = nodeToReport.getFirstChild().getTextLength() + return nodeToReport.getTextOffset() + quotesLength + code.offsetOf(this) + } +} + +/** + * Calculates an offset from the start of a text for a position, + * defined by line and offset in that line. + */ +private fun String.offsetOf(position: CodePosition): Int { + var i = 0 + var lineCount = 0 + var offsetInLine = 0 + + while (i < length()) { + val c = charAt(i) + + if (lineCount == position.line && offsetInLine == position.offset) { + return i + } + + i++ + offsetInLine++ + + if (isEndOfLine(c.toInt())) { + offsetInLine = 0 + lineCount++ + assert(lineCount <= position.line) + } + } + + return length() +} + +private val JetExpression.isConstantStringLiteral: Boolean + get() = this is JetStringTemplateExpression && getEntries().all { it is JetLiteralStringTemplateEntry } + +open class JsCallData(val reportRange: TextRange, val message: String) + +class JsCallDataWithCode( + reportRange: TextRange, + message: String, + val code: String, + val codeRange: TextRange +) : JsCallData(reportRange, message) diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCodePositioningStrategy.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCodePositioningStrategy.kt index 190de00b1cc..183d28b2264 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCodePositioningStrategy.kt +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsCodePositioningStrategy.kt @@ -20,13 +20,17 @@ import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import org.jetbrains.kotlin.diagnostics.PositioningStrategy import org.jetbrains.kotlin.psi.JetExpression -import org.jetbrains.kotlin.diagnostics.DiagnosticWithParameters2 +import org.jetbrains.kotlin.diagnostics.DiagnosticWithParameters3 import org.jetbrains.kotlin.diagnostics.ParametrizedDiagnostic +import org.jetbrains.kotlin.resolve.diagnostics.JsCallData +import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1 +import org.jetbrains.kotlin.diagnostics.DiagnosticWithParameters1 public object JsCodePositioningStrategy : PositioningStrategy() { override fun markDiagnostic(diagnostic: ParametrizedDiagnostic): List { [suppress("UNCHECKED_CAST")] - val diagnosticWithParameters = diagnostic as DiagnosticWithParameters2> - return diagnosticWithParameters.getB() + val diagnosticWithParameters = diagnostic as DiagnosticWithParameters1 + val textRange = diagnosticWithParameters.getA().reportRange + return listOf(textRange) } } diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java index 8ee67a82a0e..d2ef6beb845 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/reference/CallExpressionTranslator.java @@ -22,18 +22,13 @@ import com.google.dart.compiler.common.SourceInfoImpl; import com.google.gwt.dev.js.AbortParsingException; import com.google.gwt.dev.js.JsParser; import com.google.gwt.dev.js.JsParserException; +import com.google.gwt.dev.js.rhino.CodePosition; import com.google.gwt.dev.js.rhino.ErrorReporter; -import com.google.gwt.dev.js.rhino.EvaluatorException; -import com.intellij.openapi.util.TextRange; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.builtins.InlineStrategy; import org.jetbrains.kotlin.builtins.InlineUtil; import org.jetbrains.kotlin.descriptors.*; -import org.jetbrains.kotlin.diagnostics.DiagnosticFactory2; -import org.jetbrains.kotlin.diagnostics.DiagnosticSink; -import org.jetbrains.kotlin.diagnostics.ParametrizedDiagnostic; -import org.jetbrains.kotlin.js.resolve.diagnostics.ErrorsJs; import org.jetbrains.kotlin.js.translate.callTranslator.CallTranslator; import org.jetbrains.kotlin.js.translate.context.TranslationContext; import org.jetbrains.kotlin.psi.JetCallExpression; @@ -50,10 +45,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import static com.google.gwt.dev.js.rhino.Utils.isEndOfLine; -import static org.jetbrains.kotlin.js.descriptors.DescriptorsPackage.getJS_PATTERN; import static org.jetbrains.kotlin.js.translate.utils.BindingUtils.getCompileTimeValue; import static org.jetbrains.kotlin.resolve.calls.callUtil.CallUtilPackage.getFunctionResolvedCallWithAssert; +import static org.jetbrains.kotlin.resolve.diagnostics.JsCallChecker.isJsCall; public final class CallExpressionTranslator extends AbstractCallExpressionTranslator { @@ -63,7 +57,9 @@ public final class CallExpressionTranslator extends AbstractCallExpressionTransl @Nullable JsExpression receiver, @NotNull TranslationContext context ) { - if (matchesJsCode(expression, context)) { + ResolvedCall resolvedCall = getFunctionResolvedCallWithAssert(expression, context.bindingContext()); + + if (isJsCall(resolvedCall)) { return (new CallExpressionTranslator(expression, receiver, context)).translateJsCode(); } @@ -105,16 +101,6 @@ public final class CallExpressionTranslator extends AbstractCallExpressionTransl return false; } - private static boolean matchesJsCode( - @NotNull JetCallExpression expression, - @NotNull TranslationContext context - ) { - FunctionDescriptor descriptor = getFunctionResolvedCallWithAssert(expression, context.bindingContext()) - .getResultingDescriptor(); - - return getJS_PATTERN().apply(descriptor); - } - private CallExpressionTranslator( @NotNull JetCallExpression expression, @Nullable JsExpression receiver, @@ -132,11 +118,7 @@ public final class CallExpressionTranslator extends AbstractCallExpressionTransl private JsNode translateJsCode() { List arguments = expression.getValueArguments(); JetExpression argumentExpression = arguments.get(0).getArgumentExpression(); - - if (!(argumentExpression instanceof JetStringTemplateExpression)) { - context().getTrace().report(ErrorsJs.JSCODE_ARGUMENT_SHOULD_BE_LITERAL.on(expression)); - return program().getEmptyExpression(); - } + assert argumentExpression instanceof JetStringTemplateExpression; List statements = parseJsCode((JetStringTemplateExpression) argumentExpression); int size = statements.size(); @@ -161,7 +143,21 @@ public final class CallExpressionTranslator extends AbstractCallExpressionTransl assert jsCode instanceof String: "jsCode must be compile time string"; List statements = new ArrayList(); - ErrorReporter errorReporter = new JsCodeErrorReporter(jsCodeExpression, (String) jsCode, context().getTrace()); + ErrorReporter errorReporter = new ErrorReporter() { + @Override + public void warning( + @NotNull String message, @NotNull CodePosition startPosition, @NotNull CodePosition endPosition + ) { + + } + + @Override + public void error( + @NotNull String message, @NotNull CodePosition startPosition, @NotNull CodePosition endPosition + ) { + throw new IllegalStateException("JS parser error in backend (must have been checked in frontend): " + message); + } + }; try { SourceInfoImpl info = new SourceInfoImpl(null, 0, 0, 0, 0); @@ -179,83 +175,4 @@ public final class CallExpressionTranslator extends AbstractCallExpressionTransl return statements; } - - private static class JsCodeErrorReporter implements ErrorReporter { - - @NotNull - private final JetExpression jsCodeExpression; - - @NotNull - private final String code; - - @NotNull - private final DiagnosticSink trace; - - JsCodeErrorReporter(@NotNull JetStringTemplateExpression jsCodeExpression, - @NotNull String code, - @NotNull DiagnosticSink trace - ) { - this.jsCodeExpression = jsCodeExpression; - this.code = code; - this.trace = trace; - } - - @Override - public void error(String message, String sourceName, int line, String lineSource, int lineOffset) { - ParametrizedDiagnostic diagnostic = getDiagnostic(ErrorsJs.JSCODE_ERROR, message, line, lineOffset); - trace.report(diagnostic); - throw new AbortParsingException(); - } - - @Override - public void warning(String message, String sourceName, int line, String lineSource, int lineOffset) { - ParametrizedDiagnostic diagnostic = getDiagnostic(ErrorsJs.JSCODE_WARNING, message, line, lineOffset); - trace.report(diagnostic); - } - - private ParametrizedDiagnostic getDiagnostic( - @NotNull DiagnosticFactory2> diagnosticFactory, - String message, - int line, - int lineOffset - ) { - int offset = jsCodeExpression.getTextOffset() + offsetFromStart(code, line, lineOffset); - - assert jsCodeExpression instanceof JetStringTemplateExpression: "js argument is expected to be compile-time string literal"; - int quotesLength = jsCodeExpression.getFirstChild().getTextLength(); - offset += quotesLength; - - TextRange textRange = new TextRange(offset, offset + 1); - return diagnosticFactory.on(jsCodeExpression, message, Collections.singletonList(textRange)); - } - - /** - * Calculates an offset from the start of a text for a position, - * defined by line and offset in that line. - */ - private static int offsetFromStart(String text, int line, int offset) { - int i = 0; - int lineCount = 0; - int offsetInLine = 0; - - while (i < text.length()) { - char c = text.charAt(i); - - if (lineCount == line && offsetInLine == offset) { - return i; - } - - if (isEndOfLine(c)) { - offsetInLine = 0; - lineCount++; - assert lineCount <= line; - } - - i++; - offsetInLine++; - } - - return text.length(); - } - } }