JS: check js() call at frontend

This commit is contained in:
Alexey Tsvetkov
2015-02-19 13:54:13 +03:00
parent f5eb39e1f1
commit 05ef358177
7 changed files with 219 additions and 116 deletions
+1
View File
@@ -21,6 +21,7 @@
<element id="module-output" name="util.runtime" />
<element id="module-output" name="j2k" />
<element id="module-output" name="js.frontend" />
<element id="module-output" name="js.parser" />
<element id="module-output" name="cli-common" />
<element id="module-output" name="idea-jps-common" />
<element id="module-output" name="preloader" />
+1
View File
@@ -10,6 +10,7 @@
<orderEntry type="library" scope="PROVIDED" name="intellij-core" level="project" />
<orderEntry type="module" module-name="frontend" />
<orderEntry type="module" module-name="js.dart-ast" exported="" />
<orderEntry type="module" module-name="js.parser" exported="" />
<orderEntry type="module" module-name="util" />
</component>
</module>
@@ -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()
)
@@ -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<JetDeclaration> NATIVE_GETTER_RETURN_TYPE_SHOULD_BE_NULLABLE = DiagnosticFactory0.create(ERROR, DECLARATION_RETURN_TYPE);
DiagnosticFactory0<JetDeclaration> NATIVE_SETTER_WRONG_RETURN_TYPE = DiagnosticFactory0.create(ERROR, DECLARATION_RETURN_TYPE);
DiagnosticFactory2<JetElement, Integer, String> NATIVE_INDEXER_WRONG_PARAMETER_COUNT = DiagnosticFactory2.create(ERROR, DECLARATION_SIGNATURE_OR_DEFAULT);
DiagnosticFactory2<JetExpression, String, List<TextRange>> JSCODE_ERROR = DiagnosticFactory2.create(ERROR, JsCodePositioningStrategy.INSTANCE$);
DiagnosticFactory2<JetExpression, String, List<TextRange>> JSCODE_WARNING = DiagnosticFactory2.create(WARNING, JsCodePositioningStrategy.INSTANCE$);
DiagnosticFactory0<JetExpression> JSCODE_ARGUMENT_SHOULD_BE_LITERAL = DiagnosticFactory0.create(ERROR, CALL_EXPRESSION);
DiagnosticFactory1<JetExpression, JsCallData> JSCODE_ERROR = DiagnosticFactory1.create(ERROR, JsCodePositioningStrategy.INSTANCE$);
DiagnosticFactory1<JetExpression, JsCallData> JSCODE_WARNING = DiagnosticFactory1.create(WARNING, JsCodePositioningStrategy.INSTANCE$);
DiagnosticFactory0<JetExpression> JSCODE_ARGUMENT_SHOULD_BE_CONSTANT = DiagnosticFactory0.create(ERROR, DEFAULT);
@SuppressWarnings("UnusedDeclaration")
Object _initializer = new Object() {
@@ -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 <F : CallableDescriptor?> ResolvedCall<F>.isJsCall(): Boolean {
val descriptor = getResultingDescriptor()
return descriptor is SimpleFunctionDescriptor && JS_PATTERN.apply(descriptor)
}
}
override fun <F : CallableDescriptor?> check(resolvedCall: ResolvedCall<F>, 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<JetExpression, JsCallData>,
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)
@@ -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<PsiElement>() {
override fun markDiagnostic(diagnostic: ParametrizedDiagnostic<out PsiElement>): List<TextRange> {
[suppress("UNCHECKED_CAST")]
val diagnosticWithParameters = diagnostic as DiagnosticWithParameters2<JetExpression, String, List<TextRange>>
return diagnosticWithParameters.getB()
val diagnosticWithParameters = diagnostic as DiagnosticWithParameters1<JetExpression, JsCallData>
val textRange = diagnosticWithParameters.getA().reportRange
return listOf(textRange)
}
}
@@ -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<? extends FunctionDescriptor> 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<? extends ValueArgument> 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<JsStatement> 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<JsStatement> statements = new ArrayList<JsStatement>();
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<JetExpression> 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<JetExpression> diagnostic = getDiagnostic(ErrorsJs.JSCODE_WARNING, message, line, lineOffset);
trace.report(diagnostic);
}
private ParametrizedDiagnostic<JetExpression> getDiagnostic(
@NotNull DiagnosticFactory2<JetExpression, String, List<TextRange>> 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();
}
}
}