blob: cdaf22d53dba7a413c34eb76d3e78d90a99cb1ff [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.Collections;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
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.OffsetRange;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.php.editor.CodeUtils;
import org.netbeans.modules.php.editor.api.QualifiedName;
import org.netbeans.modules.php.editor.model.Model;
import org.netbeans.modules.php.editor.model.ModelUtils;
import org.netbeans.modules.php.editor.model.TypeScope;
import org.netbeans.modules.php.editor.parser.PHPParseResult;
import org.netbeans.modules.php.editor.parser.astnodes.Expression;
import org.netbeans.modules.php.editor.parser.astnodes.FunctionInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.IgnoreError;
import org.netbeans.modules.php.editor.parser.astnodes.MethodInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.visitors.DefaultVisitor;
import org.openide.filesystems.FileObject;
import org.openide.util.NbBundle;
/**
*
* @author Ondrej Brejla <obrejla@netbeans.org>
*/
public class ErrorControlOperatorHint extends HintRule {
private static final String HINT_ID = "error.control.operator.hint"; //NOI18N
private static final Set<IgnoreErrorValidator> IGNORE_ERROR_VALIDATORS = new HashSet<>();
static {
IGNORE_ERROR_VALIDATORS.add(new FunctionInvocationValidator("fopen")); //NOI18N
IGNORE_ERROR_VALIDATORS.add(new FunctionInvocationValidator("unlink")); //NOI18N
IGNORE_ERROR_VALIDATORS.add(new FunctionInvocationValidator("mysql_connect")); //NOI18N
IGNORE_ERROR_VALIDATORS.add(new FunctionInvocationValidator("ob_end_clean")); //NOI18N
IGNORE_ERROR_VALIDATORS.add(new FunctionInvocationValidator("ob_end_flush")); //NOI18N
IGNORE_ERROR_VALIDATORS.add(new FunctionInvocationValidator("mkdir")); //NOI18N
IGNORE_ERROR_VALIDATORS.add(new FunctionInvocationValidator("iconv")); //NOI18N
IGNORE_ERROR_VALIDATORS.add(new MethodInvocationValidator(QualifiedName.create("\\DOMDocument"), "loadHTML")); //NOI18N
IGNORE_ERROR_VALIDATORS.add(new MethodInvocationValidator(QualifiedName.create("\\DOMDocument"), "loadHTMLFile")); //NOI18N
IGNORE_ERROR_VALIDATORS.add(new MethodInvocationValidator(QualifiedName.create("\\DOMDocument"), "loadXML")); //NOI18N
}
@Override
public void invoke(PHPRuleContext context, List<Hint> hints) {
PHPParseResult phpParseResult = (PHPParseResult) context.parserResult;
if (phpParseResult.getProgram() != null) {
FileObject fileObject = phpParseResult.getSnapshot().getSource().getFileObject();
if (fileObject != null) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
CheckVisitor checkVisitor = new CheckVisitor(fileObject, context.doc, phpParseResult.getModel());
phpParseResult.getProgram().accept(checkVisitor);
if (CancelSupport.getDefault().isCancelled()) {
return;
}
hints.addAll(checkVisitor.getHints());
}
}
}
private final class CheckVisitor extends DefaultVisitor {
private final FileObject fileObject;
private final BaseDocument baseDocument;
private final Model model;
private final List<Hint> hints;
private CheckVisitor(FileObject fileObject, BaseDocument baseDocument, Model model) {
this.fileObject = fileObject;
this.baseDocument = baseDocument;
this.model = model;
this.hints = new ArrayList<>();
}
private Collection<? extends Hint> getHints() {
return hints;
}
@Override
@NbBundle.Messages("ErrorControlOperatorHintText=Error Control Operator Misused")
public void visit(IgnoreError node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
super.visit(node);
if (!isValidCase(node)) {
createHint(node);
}
}
private void createHint(IgnoreError node) {
OffsetRange offsetRange = new OffsetRange(node.getStartOffset(), node.getEndOffset());
if (showHint(offsetRange, baseDocument)) {
hints.add(new Hint(
ErrorControlOperatorHint.this,
Bundle.ErrorControlOperatorHintText(),
fileObject,
offsetRange,
Collections.<HintFix>singletonList(new Fix(node, baseDocument)),
500));
}
}
private boolean isValidCase(IgnoreError node) {
boolean result = false;
for (IgnoreErrorValidator ignoreErrorValidator : IGNORE_ERROR_VALIDATORS) {
if (ignoreErrorValidator.isValid(node, model)) {
result = true;
break;
}
}
return result;
}
}
private static final class Fix implements HintFix {
private static final String ERROR_CONTROL_OPERATOR = "@"; //NOI18N
private final IgnoreError node;
private final BaseDocument baseDocument;
private Fix(IgnoreError node, BaseDocument baseDocument) {
this.node = node;
this.baseDocument = baseDocument;
}
@Override
@NbBundle.Messages("ErrorControlOperatorHintFix=Remove Error Control Operator")
public String getDescription() {
return Bundle.ErrorControlOperatorHintFix();
}
@Override
public void implement() throws Exception {
EditList editList = new EditList(baseDocument);
editList.replace(node.getStartOffset(), ERROR_CONTROL_OPERATOR.length(), "", true, 0); //NOI18N
editList.apply();
}
@Override
public boolean isSafe() {
return true;
}
@Override
public boolean isInteractive() {
return false;
}
}
private interface IgnoreErrorValidator {
boolean isValid(IgnoreError node, Model model);
}
private static final class FunctionInvocationValidator implements IgnoreErrorValidator {
private final String functionName;
public FunctionInvocationValidator(String functionName) {
assert functionName != null;
this.functionName = functionName.toLowerCase();
}
@Override
public boolean isValid(IgnoreError node, Model model) {
assert node != null;
assert model != null;
boolean result = false;
Expression expression = node.getExpression();
if (expression instanceof FunctionInvocation) {
result = isValid((FunctionInvocation) expression);
}
return result;
}
private boolean isValid(FunctionInvocation functionInvocation) {
boolean result = false;
String functionInvocationName = CodeUtils.extractFunctionName(functionInvocation);
if (functionInvocationName != null) {
result = functionName.equals(functionInvocationName.toLowerCase());
}
return result;
}
}
private static final class MethodInvocationValidator implements IgnoreErrorValidator {
private final QualifiedName fullyQualifiedTypeName;
private final String methodName;
public MethodInvocationValidator(QualifiedName fullyQualifiedTypeName, String methodName) {
assert fullyQualifiedTypeName != null;
assert fullyQualifiedTypeName.getKind().isFullyQualified();
assert methodName != null;
this.fullyQualifiedTypeName = fullyQualifiedTypeName;
this.methodName = methodName;
}
@Override
public boolean isValid(IgnoreError node, Model model) {
assert node != null;
assert model != null;
boolean result = false;
Expression expression = node.getExpression();
if (expression instanceof MethodInvocation) {
result = isValid((MethodInvocation) expression, model);
}
return result;
}
private boolean isValid(MethodInvocation methodInvocation, Model model) {
boolean result = false;
String methodInvocationName = CodeUtils.extractFunctionName(methodInvocation.getMethod());
if (methodName.equals(methodInvocationName)) {
Collection<? extends TypeScope> types = ModelUtils.resolveType(model, methodInvocation);
TypeScope type = ModelUtils.getFirst(types);
if (type != null) {
result = fullyQualifiedTypeName.equals(type.getFullyQualifiedName());
}
}
return result;
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("ErrorControlOperatorHintDesc=Error control operator disables all error reporting for an affected expression. "
+ "It should be used only for some special cases (like fopen(), unlink(), etc.). "
+ "Otherwise it's a cause of an unexpected behavior of the application. Handle your errors in a common way.")
public String getDescription() {
return Bundle.ErrorControlOperatorHintDesc();
}
@Override
@NbBundle.Messages("ErrorControlOperatorHintDisp=Error Control Operator Misused")
public String getDisplayName() {
return Bundle.ErrorControlOperatorHintDisp();
}
}