blob: 269a1735a52170900a31116698ae1c6756517ee8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.php.editor.verification;
import java.util.Collection;
import java.util.ArrayList;
import java.util.List;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.EditList;
import org.netbeans.modules.csl.api.Hint;
import org.netbeans.modules.csl.api.HintFix;
import org.netbeans.modules.csl.api.HintSeverity;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.php.editor.CodeUtils;
import org.netbeans.modules.php.editor.model.Model;
import org.netbeans.modules.php.editor.model.ModelUtils;
import org.netbeans.modules.php.editor.model.VariableName;
import org.netbeans.modules.php.editor.model.VariableScope;
import org.netbeans.modules.php.editor.model.impl.Type;
import org.netbeans.modules.php.editor.parser.PHPParseResult;
import org.netbeans.modules.php.editor.parser.astnodes.ASTNode;
import org.netbeans.modules.php.editor.parser.astnodes.Expression;
import org.netbeans.modules.php.editor.parser.astnodes.InfixExpression;
import org.netbeans.modules.php.editor.parser.astnodes.InfixExpression.OperatorType;
import org.netbeans.modules.php.editor.parser.astnodes.Variable;
import org.netbeans.modules.php.editor.parser.astnodes.visitors.DefaultVisitor;
import org.openide.filesystems.FileObject;
import org.openide.util.NbBundle.Messages;
/**
*
* @author Ondrej Brejla <obrejla@netbeans.org>
*/
public class IdenticalComparisonSuggestion extends SuggestionRule {
private static final String HINT_ID = "Identical.Comparison.Hint"; //NOI18N
@Override
public void invoke(PHPRuleContext context, List<Hint> hints) {
PHPParseResult phpParseResult = (PHPParseResult) context.parserResult;
if (phpParseResult.getProgram() == null) {
return;
}
final BaseDocument doc = context.doc;
int caretOffset = getCaretOffset();
OffsetRange lineBounds = VerificationUtils.createLineBounds(caretOffset, doc);
if (lineBounds.containsInclusive(caretOffset)) {
FileObject fileObject = phpParseResult.getSnapshot().getSource().getFileObject();
if (fileObject == null) {
return;
}
if (CancelSupport.getDefault().isCancelled()) {
return;
}
CheckVisitor checkVisitor = new CheckVisitor(fileObject, phpParseResult.getModel(), context.doc, lineBounds);
phpParseResult.getProgram().accept(checkVisitor);
if (CancelSupport.getDefault().isCancelled()) {
return;
}
hints.addAll(checkVisitor.getHints());
}
}
private class CheckVisitor extends DefaultVisitor {
private final FileObject fileObject;
private final Model model;
private final List<FixInfo> fixInfos = new ArrayList<>();
private final List<Hint> hints = new ArrayList<>();
private final BaseDocument doc;
private final OffsetRange lineRange;
public CheckVisitor(FileObject fileObject, Model model, BaseDocument doc, OffsetRange lineRange) {
this.fileObject = fileObject;
this.model = model;
this.doc = doc;
this.lineRange = lineRange;
}
@Messages("IdenticalComparisonDesc=Comparison with \"equal (==)\" operator should be avoided, use \"identical (===)\" operator instead")
public List<Hint> getHints() {
for (FixInfo fixInfo : fixInfos) {
hints.add(new Hint(IdenticalComparisonSuggestion.this, Bundle.IdenticalComparisonDesc(), fileObject, fixInfo.getScopeRange(), createFixes(fixInfo), 500));
}
return hints;
}
private List<HintFix> createFixes(FixInfo fixInfo) {
List<HintFix> hintFixes = new ArrayList<>();
hintFixes.add(new WithoutTypeFix(fixInfo, doc));
if (!fixInfo.getTypeName().isEmpty()) {
hintFixes.add(new WithRightTypeFix(fixInfo, doc));
}
return hintFixes;
}
@Override
public void scan(ASTNode node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (node != null && (VerificationUtils.isBefore(node.getStartOffset(), lineRange.getEnd()))) {
super.scan(node);
}
}
@Override
public void visit(InfixExpression node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
if (lineRange.containsInclusive(node.getStartOffset())) {
processExpression(node);
}
}
private void processExpression(InfixExpression node) {
if (node.getOperator().equals(OperatorType.IS_EQUAL)) {
int start = node.getLeft().getEndOffset();
int length = node.getRight().getStartOffset() - start;
OffsetRange scopeRange = new OffsetRange(node.getLeft().getStartOffset(), node.getRight().getEndOffset());
fixInfos.add(new FixInfo(scopeRange, start, length, extractTypeName(node.getLeft())));
}
scan(node.getLeft());
scan(node.getRight());
}
private String extractTypeName(Expression node) {
String retval = ""; //NOI18N
if (node instanceof Variable) {
Variable variable = (Variable) node;
retval = extractTypeName(variable);
}
return retval;
}
private String extractTypeName(Variable variable) {
VariableScope variableScope = model.getVariableScope(variable.getStartOffset());
Collection<? extends VariableName> declaredVariables = variableScope.getDeclaredVariables();
String originalVariableName = CodeUtils.extractVariableName(variable);
VariableName exactVariable = getExactVariable(declaredVariables, originalVariableName);
Collection<? extends String> typeNames = getTypeNames(exactVariable, variable.getStartOffset());
return getCastableType(typeNames);
}
private VariableName getExactVariable(Collection<? extends VariableName> declaredVariables, String originalVariableName) {
VariableName retval = null;
for (VariableName variableName : declaredVariables) {
if (variableName.getName().equals(originalVariableName)) {
retval = variableName;
break;
}
}
return retval;
}
private Collection<? extends String> getTypeNames(VariableName variableName, int offset) {
Collection<? extends String> retval = null;
if (variableName != null) {
retval = variableName.getTypeNames(offset);
}
return retval;
}
private String getCastableType(Collection<? extends String> typeNames) {
String retval = ""; //NOI18N
if (typeNames != null && typeNames.size() == 1) {
String typeName = ModelUtils.getFirst(typeNames);
if (typeName != null) {
retval = resolveCastableType(typeName); //NOI18N
}
}
return retval;
}
private String resolveCastableType(String typeName) {
String retval = ""; //NOI18N
switch (typeName) {
case Type.INT:
case Type.INTEGER:
case Type.DOUBLE:
case Type.FLOAT:
case Type.BOOL:
case Type.BOOLEAN:
case Type.STRING:
case Type.ARRAY:
retval = typeName;
break;
case Type.REAL:
retval = Type.FLOAT;
break;
default:
// no-op
}
return retval;
}
}
private abstract class TypeFix implements HintFix {
protected final BaseDocument doc;
protected final FixInfo fixInfo;
public TypeFix(FixInfo fixInfo, BaseDocument doc) {
this.fixInfo = fixInfo;
this.doc = doc;
}
@Override
public boolean isSafe() {
return true;
}
@Override
public boolean isInteractive() {
return false;
}
}
private class WithRightTypeFix extends TypeFix {
public WithRightTypeFix(FixInfo fixInfo, BaseDocument doc) {
super(fixInfo, doc);
}
@Override
@Messages({
"# {0} - Type name",
"WithRightTypeFixDesc=Fix comparison: === ({0}) "
})
public String getDescription() {
return Bundle.WithRightTypeFixDesc(fixInfo.getTypeName());
}
@Override
public void implement() throws Exception {
EditList edits = new EditList(doc);
edits.replace(fixInfo.getStart(), fixInfo.getLength(), " === (" + fixInfo.getTypeName() + ") ", true, 0); //NOI18N
edits.apply();
}
}
private class WithoutTypeFix extends TypeFix {
public WithoutTypeFix(FixInfo fixInfo, BaseDocument doc) {
super(fixInfo, doc);
}
@Override
@Messages("WithoutTypeFixDesc=Fix comparison: ===")
public String getDescription() {
return Bundle.WithoutTypeFixDesc();
}
@Override
public void implement() throws Exception {
EditList edits = new EditList(doc);
edits.replace(fixInfo.getStart(), fixInfo.getLength(), " === ", true, 0); //NOI18N
edits.apply();
}
}
private static class FixInfo {
private final String typeName;
private final int length;
private final int start;
private final OffsetRange scopeRange;
public FixInfo(OffsetRange scopeRange, int start, int length, String typeName) {
this.scopeRange = scopeRange;
this.start = start;
this.length = length;
this.typeName = typeName;
}
public int getStart() {
return start;
}
public int getLength() {
return length;
}
public String getTypeName() {
return typeName;
}
public OffsetRange getScopeRange() {
return scopeRange;
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@Messages("IdenticalComparisonHintDesc=You should use \"identical\" instead of \"equal\" comparison to have better control over your code.")
public String getDescription() {
return Bundle.IdenticalComparisonHintDesc();
}
@Override
@Messages("IdenticalComparisonHintDispName=Identical Comparisons")
public String getDisplayName() {
return Bundle.IdenticalComparisonHintDispName();
}
@Override
public HintSeverity getDefaultSeverity() {
return HintSeverity.CURRENT_LINE_WARNING;
}
}