mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-192155 Contract checker: highlight the relevant part of contract
This commit is contained in:
+27
-12
@@ -6,9 +6,11 @@ import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
|
||||
import com.intellij.codeInspection.ProblemsHolder;
|
||||
import com.intellij.codeInspection.dataFlow.StandardMethodContract.ValueConstraint;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.siyeh.ig.psiutils.ExpressionUtils;
|
||||
import one.util.streamex.StreamEx;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -57,11 +59,16 @@ public class ContractInspection extends AbstractBaseJavaLocalInspectionTool {
|
||||
|
||||
String text = AnnotationUtil.getStringAttributeValue(annotation, null);
|
||||
if (StringUtil.isNotEmpty(text)) {
|
||||
String error = checkContract(method, text);
|
||||
ParseException error = checkContract(method, text);
|
||||
if (error != null) {
|
||||
PsiAnnotationMemberValue value = annotation.findAttributeValue(null);
|
||||
assert value != null;
|
||||
holder.registerProblem(value, error);
|
||||
TextRange actualRange = null;
|
||||
if (value instanceof PsiExpression && error.getRange() != null) {
|
||||
actualRange = ExpressionUtils
|
||||
.findStringLiteralRange((PsiExpression)value, error.getRange().getStartOffset(), error.getRange().getEndOffset());
|
||||
}
|
||||
holder.registerProblem(value, actualRange, error.getMessage());
|
||||
}
|
||||
}
|
||||
checkMutationContract(annotation, method);
|
||||
@@ -88,21 +95,23 @@ public class ContractInspection extends AbstractBaseJavaLocalInspectionTool {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String checkContract(PsiMethod method, String text) {
|
||||
public static ParseException checkContract(PsiMethod method, String text) {
|
||||
List<StandardMethodContract> contracts;
|
||||
try {
|
||||
contracts = parseContract(text);
|
||||
}
|
||||
catch (ParseException e) {
|
||||
return e.getMessage();
|
||||
return e;
|
||||
}
|
||||
PsiParameter[] parameters = method.getParameterList().getParameters();
|
||||
int paramCount = parameters.length;
|
||||
List<Conditions> possibleConditions = Collections.singletonList(new Conditions(paramCount));
|
||||
for (StandardMethodContract contract : contracts) {
|
||||
for (int clauseIndex = 0; clauseIndex < contracts.size(); clauseIndex++) {
|
||||
StandardMethodContract contract = contracts.get(clauseIndex);
|
||||
if (contract.getParameterCount() != paramCount) {
|
||||
return "Method takes " + paramCount + " parameters, " +
|
||||
"while contract clause '" + contract + "' expects " + contract.getParameterCount();
|
||||
return ParseException.forClause("Method takes " + paramCount + " parameters, " +
|
||||
"while contract clause '" + contract + "' expects " + contract.getParameterCount(), text,
|
||||
clauseIndex);
|
||||
}
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
ValueConstraint constraint = contract.getParameterConstraint(i);
|
||||
@@ -113,27 +122,33 @@ public class ContractInspection extends AbstractBaseJavaLocalInspectionTool {
|
||||
case NULL_VALUE:
|
||||
case NOT_NULL_VALUE:
|
||||
if (type instanceof PsiPrimitiveType) {
|
||||
return "Contract clause '"+contract+"': parameter #"+(i+1)+" has primitive type '"+type.getPresentableText()+"'";
|
||||
String message =
|
||||
"Contract clause '" + contract + "': parameter #" + (i + 1) + " has primitive type '" + type.getPresentableText() + "'";
|
||||
return ParseException.forConstraint(message, text, clauseIndex, i);
|
||||
}
|
||||
break;
|
||||
case TRUE_VALUE:
|
||||
case FALSE_VALUE:
|
||||
if (!PsiType.BOOLEAN.equals(type) && !type.equalsToText(CommonClassNames.JAVA_LANG_BOOLEAN)) {
|
||||
return "Contract clause '"+contract+"': parameter #"+(i+1)+" has '"+type.getPresentableText()+"' type (expected boolean)";
|
||||
String message = "Contract clause '" + contract + "': parameter #" + (i + 1) + " has '" +
|
||||
type.getPresentableText() + "' type (expected boolean)";
|
||||
return ParseException.forConstraint(message, text, clauseIndex, i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
String problem = contract.getReturnValue().getMethodCompatibilityProblem(method);
|
||||
if (problem != null) {
|
||||
return problem;
|
||||
return ParseException.forReturnValue(problem, text, clauseIndex);
|
||||
}
|
||||
if (possibleConditions != null) {
|
||||
if (possibleConditions.isEmpty()) {
|
||||
return "Contract clause '" + contract + "' is unreachable: previous contracts cover all possible cases";
|
||||
return ParseException
|
||||
.forClause("Contract clause '" + contract + "' is unreachable: previous contracts cover all possible cases", text, clauseIndex);
|
||||
}
|
||||
if (StreamEx.of(possibleConditions).allMatch(c -> c.fitContract(contract) == null)) {
|
||||
return "Contract clause '" + contract + "' is never satisfied as its conditions are covered by previous contracts";
|
||||
return ParseException.forClause(
|
||||
"Contract clause '" + contract + "' is never satisfied as its conditions are covered by previous contracts", text, clauseIndex);
|
||||
}
|
||||
possibleConditions = StreamEx.of(possibleConditions).flatMap(c -> c.misfitContract(contract))
|
||||
.limit(DataFlowRunner.MAX_STATES_PER_BRANCH).toList();
|
||||
|
||||
+100
-18
@@ -18,6 +18,7 @@ package com.intellij.codeInspection.dataFlow;
|
||||
import com.intellij.codeInspection.dataFlow.value.DfaConstValue;
|
||||
import com.intellij.codeInspection.dataFlow.value.DfaRelationValue;
|
||||
import com.intellij.codeInspection.dataFlow.value.DfaValueFactory;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import one.util.streamex.IntStreamEx;
|
||||
@@ -98,11 +99,13 @@ public final class StandardMethodContract extends MethodContract {
|
||||
|
||||
public static List<StandardMethodContract> parseContract(String text) throws ParseException {
|
||||
List<StandardMethodContract> result = ContainerUtil.newArrayList();
|
||||
for (String clause : StringUtil.replace(text, " ", "").split(";")) {
|
||||
String[] split = StringUtil.replace(text, " ", "").split(";");
|
||||
for (int clauseIndex = 0; clauseIndex < split.length; clauseIndex++) {
|
||||
String clause = split[clauseIndex];
|
||||
String arrow = "->";
|
||||
int arrowIndex = clause.indexOf(arrow);
|
||||
if (arrowIndex < 0) {
|
||||
throw new ParseException("A contract clause must be in form arg1, ..., argN -> return-value");
|
||||
throw ParseException.forClause("A contract clause must be in form arg1, ..., argN -> return-value", text, clauseIndex);
|
||||
}
|
||||
|
||||
String beforeArrow = clause.substring(0, arrowIndex);
|
||||
@@ -111,32 +114,31 @@ public final class StandardMethodContract extends MethodContract {
|
||||
String[] argStrings = beforeArrow.split(",");
|
||||
args = new ValueConstraint[argStrings.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
args[i] = parseConstraint(argStrings[i]);
|
||||
args[i] = parseConstraint(argStrings[i], text, clauseIndex, i);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
args = new ValueConstraint[0];
|
||||
}
|
||||
result.add(new StandardMethodContract(args, parseReturnValue(clause.substring(arrowIndex + arrow.length()))));
|
||||
String returnValueString = clause.substring(arrowIndex + arrow.length());
|
||||
ContractReturnValue returnValue = ContractReturnValue.valueOf(returnValueString);
|
||||
if (returnValue == null) {
|
||||
throw ParseException.forReturnValue(
|
||||
"Return value should be one of: null, !null, true, false, this, new, paramN, fail, _. Found: " + returnValueString,
|
||||
text, clauseIndex);
|
||||
}
|
||||
result.add(new StandardMethodContract(args, returnValue));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static ContractReturnValue parseReturnValue(String returnValueString) throws ParseException {
|
||||
ContractReturnValue returnValue = ContractReturnValue.valueOf(returnValueString);
|
||||
if (returnValue == null) {
|
||||
throw new ParseException(
|
||||
"Return value should be one of: null, !null, true, false, this, new, paramN, fail, _. Found: " + returnValueString);
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private static ValueConstraint parseConstraint(String name) throws ParseException {
|
||||
private static ValueConstraint parseConstraint(String name, String text, int clauseIndex, int constraintIndex) throws ParseException {
|
||||
if (StringUtil.isEmpty(name)) throw new ParseException("Constraint should not be empty");
|
||||
for (ValueConstraint constraint : ValueConstraint.values()) {
|
||||
if (constraint.toString().equals(name)) return constraint;
|
||||
}
|
||||
throw new ParseException("Constraint should be one of: null, !null, true, false, _. Found: " + name);
|
||||
throw ParseException
|
||||
.forConstraint("Constraint should be one of: null, !null, true, false, _. Found: " + name, text, clauseIndex, constraintIndex);
|
||||
}
|
||||
|
||||
public enum ValueConstraint {
|
||||
@@ -221,8 +223,88 @@ public final class StandardMethodContract extends MethodContract {
|
||||
}
|
||||
|
||||
public static class ParseException extends Exception {
|
||||
private ParseException(String message) {
|
||||
private final @Nullable TextRange myRange;
|
||||
|
||||
ParseException(String message) {
|
||||
this(message, null);
|
||||
}
|
||||
|
||||
ParseException(String message, @Nullable TextRange range) {
|
||||
super(message);
|
||||
myRange = range != null && range.isEmpty() ? null : range;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TextRange getRange() {
|
||||
return myRange;
|
||||
}
|
||||
|
||||
static ParseException forConstraint(String message, String text, int clauseNumber, int constraintNumber) {
|
||||
TextRange range = findClauseRange(text, clauseNumber);
|
||||
if (range == null) {
|
||||
return new ParseException(message);
|
||||
}
|
||||
int start = range.getStartOffset();
|
||||
while (constraintNumber > 0) {
|
||||
start = text.indexOf(',', start);
|
||||
if (start == -1) return new ParseException(message, range);
|
||||
start++;
|
||||
constraintNumber--;
|
||||
}
|
||||
int end = text.indexOf(',', start);
|
||||
if (end == -1 || end > range.getEndOffset()) {
|
||||
end = text.indexOf("->", start);
|
||||
if (end == -1 || end > range.getEndOffset()) {
|
||||
end = range.getEndOffset();
|
||||
}
|
||||
}
|
||||
if (!text.substring(start, end).trim().isEmpty()) {
|
||||
while (text.charAt(start) == ' ') start++;
|
||||
while (end > start && text.charAt(end - 1) == ' ') end--;
|
||||
}
|
||||
return new ParseException(message, new TextRange(start, end));
|
||||
}
|
||||
|
||||
static ParseException forReturnValue(String message, String text, int clauseNumber) {
|
||||
TextRange range = findClauseRange(text, clauseNumber);
|
||||
if (range == null) {
|
||||
return new ParseException(message);
|
||||
}
|
||||
int index = text.indexOf("->", range.getStartOffset());
|
||||
if (index == -1 || index > range.getEndOffset()) {
|
||||
return new ParseException(message, range);
|
||||
}
|
||||
index += "->".length();
|
||||
while (index < range.getEndOffset() && text.charAt(index) == ' ') index++;
|
||||
if (index == range.getEndOffset()) {
|
||||
return new ParseException(message, range);
|
||||
}
|
||||
return new ParseException(message, new TextRange(index, range.getEndOffset()));
|
||||
}
|
||||
|
||||
static ParseException forClause(String message, String text, int clauseNumber) {
|
||||
TextRange range = findClauseRange(text, clauseNumber);
|
||||
return range == null ? new ParseException(message) : new ParseException(message, range);
|
||||
}
|
||||
|
||||
private static TextRange findClauseRange(String text, int clauseNumber) {
|
||||
int start = 0;
|
||||
while (clauseNumber > 0) {
|
||||
start = text.indexOf(';', start);
|
||||
if (start == -1) return null;
|
||||
start++;
|
||||
clauseNumber--;
|
||||
}
|
||||
int end = text.indexOf(';', start);
|
||||
if (end == -1) {
|
||||
end = text.length();
|
||||
}
|
||||
if (text.substring(start, end).trim().isEmpty()) return new TextRange(start, end);
|
||||
|
||||
while (text.charAt(start) == ' ') start++;
|
||||
while (end > start && text.charAt(end - 1) == ' ') end--;
|
||||
|
||||
return new TextRange(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +206,11 @@ public class EditContractIntention extends BaseIntentionAction implements LowPri
|
||||
|
||||
@Nullable
|
||||
private static String getContractErrorMessage(String contract, PsiMethod method) {
|
||||
return StringUtil.isEmpty(contract) ? null : ContractInspection.checkContract(method, contract);
|
||||
if (StringUtil.isEmpty(contract)) {
|
||||
return null;
|
||||
}
|
||||
StandardMethodContract.ParseException error = ContractInspection.checkContract(method, contract);
|
||||
return error != null ? error.getMessage() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import org.jetbrains.annotations.Contract;
|
||||
|
||||
class Zoo {
|
||||
@Contract(<warning descr="Contract return value 'null': not applicable for constructor">"null->null"</warning> )
|
||||
@Contract("null-><warning descr="Contract return value 'null': not applicable for constructor">null</warning>" )
|
||||
Zoo(Object o) {}
|
||||
|
||||
@Contract("_->fail" )
|
||||
|
||||
@@ -2,13 +2,13 @@ import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
class Foo {
|
||||
@Contract(<warning descr="Contract clause 'null -> null' is unreachable: previous contracts cover all possible cases">"_ -> !null; null -> null"</warning>)
|
||||
@Contract("_ -> !null; <warning descr="Contract clause 'null -> null' is unreachable: previous contracts cover all possible cases">null -> null</warning>")
|
||||
public native String nonTrivialAfterTrivial(String x);
|
||||
|
||||
@Contract(<warning descr="Contract clause '!null -> null' is never satisfied as its conditions are covered by previous contracts">"!null -> !null; !null -> null"</warning>)
|
||||
@Contract("!null -> !null; <warning descr="Contract clause '!null -> null' is never satisfied as its conditions are covered by previous contracts">!null -> null</warning>")
|
||||
public native String repeating(String x);
|
||||
|
||||
@Contract(<warning descr="Contract clause 'true, _, _ -> fail' is never satisfied as its conditions are covered by previous contracts">"true, false, _ -> !null; true, true, _ -> null; true, _, _ -> fail"</warning>)
|
||||
@Contract("true, false, _ -> !null; true, true, _\u0020-> null; <warning descr="Contract clause 'true, _, _ -> fail' is never satisfied as its conditions are covered by previous contracts">true, _, _ -> fail</warning>")
|
||||
public native String booleanProblem(boolean x, boolean y, String z);
|
||||
|
||||
@Contract("true, false, _ -> !null; true, true, _ -> null; false, _, _ -> fail")
|
||||
@@ -17,6 +17,11 @@ class Foo {
|
||||
@Contract("true, false, _ -> !null; false, _, _ -> fail; true, true, _ -> null")
|
||||
public native String booleanOk2(boolean x, boolean y, String z);
|
||||
|
||||
@Contract(<warning descr="Contract clause 'null, null, _, null, !null -> fail' is never satisfied as its conditions are covered by previous contracts">"null, null, null, null, null -> null; null, null, !null, null, _ -> null; null, null, null, null, !null -> !null; null, null, _, null, !null -> fail"</warning>)
|
||||
static final String MY_LOVELY_CONTRACT = "null, null, !null, null, _ -> null; ";
|
||||
|
||||
@Contract("null, null, null, null, null -> null; "+
|
||||
MY_LOVELY_CONTRACT+
|
||||
"null, null, null, null, !null -> !null; "+
|
||||
("<warning descr="Contract clause 'null, null, _, null, !null -> fail' is never satisfied as its conditions are covered by previous contracts">null, null, _, null, !null -> fail</warning>"))
|
||||
public native String test(String a, String b, String c, String d, String e);
|
||||
}
|
||||
|
||||
@@ -2,56 +2,56 @@ import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
class Foo {
|
||||
@Contract(<warning descr="A contract clause must be in form arg1, ..., argN -> return-value">"a"</warning>)
|
||||
@Contract("<warning descr="A contract clause must be in form arg1, ..., argN -> return-value">a</warning>")
|
||||
void malformedContract() {}
|
||||
|
||||
@Contract(<warning descr="Method takes 2 parameters, while contract clause 'null -> _' expects 1">"null -> _"</warning>)
|
||||
@Contract("<warning descr="Method takes 2 parameters, while contract clause 'null -> _' expects 1">null -> _</warning>")
|
||||
void wrongParameterCount(Object a, boolean b) {}
|
||||
|
||||
@Contract(pure=true)
|
||||
void voidPureMethod() {}
|
||||
|
||||
@Contract(<warning descr="Contract return value 'null': not applicable for primitive return type 'void'">"->null"</warning>)
|
||||
@Contract("-><warning descr="Contract return value 'null': not applicable for primitive return type 'void'">null</warning>")
|
||||
public native void throwMe();
|
||||
|
||||
@Contract(<warning descr="Contract return value 'null': not applicable for primitive return type 'boolean'">"->null"</warning>)
|
||||
@Contract("-><warning descr="Contract return value 'null': not applicable for primitive return type 'boolean'">null</warning>")
|
||||
public native boolean wrongReturnType();
|
||||
|
||||
@Contract(<warning descr="Contract return value 'true': method return type must be 'boolean'">"->true"</warning>)
|
||||
@Contract("-><warning descr="Contract return value 'true': method return type must be 'boolean'">true</warning>")
|
||||
public native String wrongReturnType2();
|
||||
|
||||
@Contract(<warning descr="Contract return value 'param1': not applicable for method which has 0 parameters">"->param1"</warning>)
|
||||
@Contract("-><warning descr="Contract return value 'param1': not applicable for method which has 0 parameters">param1</warning>")
|
||||
public native String absentParameter();
|
||||
|
||||
@Contract(<warning descr="Contract return value 'param2': not applicable for method which has 1 parameter">"_->param2"</warning>)
|
||||
@Contract("_-><warning descr="Contract return value 'param2': not applicable for method which has 1 parameter">param2</warning>")
|
||||
public native String absentParameter2(String x);
|
||||
|
||||
@Contract(<warning descr="Contract return value 'param1': return type 'String' must be assignable from parameter type 'CharSequence'">"_->param1"</warning>)
|
||||
@Contract("_-><warning descr="Contract return value 'param1': return type 'String' must be assignable from parameter type 'CharSequence'">param1</warning>")
|
||||
public native String wrongParameterType(CharSequence x);
|
||||
|
||||
@Contract("_->param1")
|
||||
public native Object okParameterType(Integer x);
|
||||
|
||||
@Contract(<warning descr="Contract return value 'new': not applicable for primitive return type 'boolean'">"->new"</warning>)
|
||||
@Contract("-><warning descr="Contract return value 'new': not applicable for primitive return type 'boolean'">new</warning>")
|
||||
public native boolean wrongReturnTypeNew();
|
||||
|
||||
@Contract(<warning descr="Contract return value 'this': not applicable for primitive return type 'boolean'">"->this"</warning>)
|
||||
@Contract("-><warning descr="Contract return value 'this': not applicable for primitive return type 'boolean'">this</warning>")
|
||||
public native boolean wrongReturnTypeThis();
|
||||
|
||||
@Contract(<warning descr="Contract return value 'this': method return type should be compatible with method containing class">"->this"</warning>)
|
||||
@Contract("-><warning descr="Contract return value 'this': method return type should be compatible with method containing class">this</warning>")
|
||||
public native String wrongReturnTypeThis2();
|
||||
|
||||
public native Foo okReturnTypeThis();
|
||||
|
||||
@Contract(<warning descr="Contract return value 'this': not applicable for static method">"->this"</warning>)
|
||||
@Contract("-><warning descr="Contract return value 'this': not applicable for static method">this</warning>")
|
||||
public native static Foo staticThis();
|
||||
|
||||
@Contract(<warning descr="Return value should be one of: null, !null, true, false, this, new, paramN, fail, _. Found: foo">"->foo"</warning>)
|
||||
@Contract("-><warning descr="Return value should be one of: null, !null, true, false, this, new, paramN, fail, _. Found: foo">foo</warning>")
|
||||
public native void invalidReturn();
|
||||
|
||||
@Contract(<warning descr="Contract clause 'true -> fail': parameter #1 has 'String' type (expected boolean)">"true -> fail"</warning>)
|
||||
@Contract("<warning descr="Contract clause 'true -> fail': parameter #1 has 'String' type (expected boolean)">true</warning> -> fail")
|
||||
public native void invalidType(String s);
|
||||
|
||||
@Contract(<warning descr="Contract clause 'null -> fail': parameter #1 has primitive type 'int'">"null -> fail"</warning>)
|
||||
@Contract("<warning descr="Contract clause 'null -> fail': parameter #1 has primitive type 'int'">null</warning> -> fail")
|
||||
public native void invalidType(int s);
|
||||
}
|
||||
|
||||
+82
-1
@@ -20,6 +20,7 @@ import com.intellij.codeInsight.ExpressionUtil;
|
||||
import com.intellij.codeInsight.NullableNotNullManager;
|
||||
import com.intellij.codeInsight.PsiEquivalenceUtil;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.search.searches.ReferencesSearch;
|
||||
import com.intellij.psi.tree.IElementType;
|
||||
@@ -1065,7 +1066,7 @@ public class ExpressionUtils {
|
||||
}
|
||||
|
||||
@Contract("null, _ -> false")
|
||||
public static boolean isMatchingChildAlwaysExecuted(@Nullable PsiExpression root, @NotNull Predicate<PsiExpression> matcher) {
|
||||
public static boolean isMatchingChildAlwaysExecuted(@Nullable PsiExpression root, @NotNull Predicate<? super PsiExpression> matcher) {
|
||||
if (root == null) return false;
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
root.accept(new JavaRecursiveElementWalkingVisitor() {
|
||||
@@ -1239,4 +1240,84 @@ public class ExpressionUtils {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the range inside the expression (relative to its start) which represents the given substring
|
||||
* assuming the expression evaluates to String.
|
||||
*
|
||||
* @param expression expression to find the range in
|
||||
* @param from start offset of substring in the String value of the expression
|
||||
* @param to end offset of substring in the String value of the expression
|
||||
* @return found range or null if cannot be found
|
||||
*/
|
||||
@Nullable
|
||||
@Contract(value = "null, _, _ -> null", pure = true)
|
||||
public static TextRange findStringLiteralRange(PsiExpression expression, int from, int to) {
|
||||
if (to < 0 || from > to) return null;
|
||||
if (expression == null || !TypeUtils.isJavaLangString(expression.getType())) return null;
|
||||
if (expression instanceof PsiLiteralExpression) {
|
||||
String value = tryCast(((PsiLiteralExpression)expression).getValue(), String.class);
|
||||
if (value == null || value.length() < from || value.length() < to) return null;
|
||||
String text = expression.getText();
|
||||
if (text.startsWith("`")) {
|
||||
// raw-string
|
||||
return new TextRange(1 + from, 1 + to);
|
||||
}
|
||||
if (text.startsWith("\"")) {
|
||||
int curOffset = 0;
|
||||
int mappedFrom = -1, mappedTo = -1;
|
||||
int end = text.length() - 1;
|
||||
int i = 1;
|
||||
while (i <= end) {
|
||||
if (curOffset == from) {
|
||||
mappedFrom = i;
|
||||
}
|
||||
if (curOffset == to) {
|
||||
mappedTo = i;
|
||||
break;
|
||||
}
|
||||
if (i == end) break;
|
||||
char c = text.charAt(i);
|
||||
if (c == '\\') {
|
||||
i++;
|
||||
if (i == end) return null;
|
||||
// like \u0020
|
||||
if (text.charAt(i) == 'u') {
|
||||
while (i < end && text.charAt(i) == 'u') i++;
|
||||
i += 3;
|
||||
}
|
||||
}
|
||||
curOffset++;
|
||||
i++;
|
||||
}
|
||||
if (mappedFrom >= 0 && mappedTo >= 0) {
|
||||
return new TextRange(mappedFrom, mappedTo);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (expression instanceof PsiParenthesizedExpression) {
|
||||
PsiExpression operand = ((PsiParenthesizedExpression)expression).getExpression();
|
||||
TextRange range = findStringLiteralRange(operand, from, to);
|
||||
return range == null ? null : range.shiftRight(operand.getStartOffsetInParent());
|
||||
}
|
||||
if (expression instanceof PsiPolyadicExpression) {
|
||||
PsiPolyadicExpression concatenation = (PsiPolyadicExpression)expression;
|
||||
if (concatenation.getOperationTokenType() != JavaTokenType.PLUS) return null;
|
||||
PsiExpression[] operands = concatenation.getOperands();
|
||||
for (PsiExpression operand : operands) {
|
||||
Object constantValue = computeConstantExpression(operand);
|
||||
if (constantValue == null) return null;
|
||||
String stringValue = constantValue.toString();
|
||||
if (from < stringValue.length()) {
|
||||
if (to > stringValue.length()) return null;
|
||||
TextRange range = findStringLiteralRange(operand, from, to);
|
||||
return range == null ? null : range.shiftRight(operand.getStartOffsetInParent());
|
||||
}
|
||||
from -= stringValue.length();
|
||||
to -= stringValue.length();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user