blob: 63c7700c9b6a67d020d9c1d6d82e4d8a8263689e [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.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.swing.JComponent;
import javax.swing.text.BadLocationException;
import org.netbeans.api.editor.document.LineDocumentUtils;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.Hint;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.php.editor.parser.PHPParseResult;
import org.netbeans.modules.php.editor.parser.astnodes.Block;
import org.netbeans.modules.php.editor.parser.astnodes.ClassDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.Comment;
import org.netbeans.modules.php.editor.parser.astnodes.FunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.InterfaceDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.LambdaFunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.Program;
import org.netbeans.modules.php.editor.parser.astnodes.TraitDeclaration;
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 abstract class TooManyLinesHint extends HintRule implements CustomisableRule {
@Override
public void invoke(PHPRuleContext context, List<Hint> result) {
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 = createVisitor(fileObject, context.doc);
phpParseResult.getProgram().accept(checkVisitor);
if (CancelSupport.getDefault().isCancelled()) {
return;
}
result.addAll(checkVisitor.getHints());
}
}
}
abstract CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument);
public static class FunctionLinesHint extends TooManyLinesHint {
private static final String HINT_ID = "Function.Lines.Hint"; //NOI18N
private static final String MAX_ALLOWED_FUNCTION_LINES = "php.verification.max.allowed.function.lines"; //NOI18N
private static final int DEFAULT_MAX_ALLOWED_FUNCTION_LINES = 20;
private Preferences preferences;
@Override
CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument) {
return new FunctionVisitor(this, fileObject, baseDocument, getMaxAllowedLines(preferences));
}
@Override
public void setPreferences(Preferences preferences) {
assert preferences != null;
this.preferences = preferences;
}
public void setMaxAllowedLines(Preferences preferences, Integer value) {
assert preferences != null;
assert value != null;
preferences.putInt(MAX_ALLOWED_FUNCTION_LINES, value);
}
private static final class FunctionVisitor extends CheckVisitor {
private final int maxAllowedLines;
public FunctionVisitor(TooManyLinesHint linesHint, FileObject fileObject, BaseDocument baseDocument, int maxAllowedLines) {
super(linesHint, fileObject, baseDocument);
this.maxAllowedLines = maxAllowedLines;
}
@Override
public void visit(FunctionDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
super.visit(node);
checkBlock(node.getBody(), new OffsetRange(node.getFunctionName().getStartOffset(), node.getFunctionName().getEndOffset()));
}
@Override
public void visit(LambdaFunctionDeclaration node) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
super.visit(node);
checkBlock(node.getBody(), new OffsetRange(node.getStartOffset(), node.getBody().getStartOffset()));
}
@Override
public void visit(InterfaceDeclaration node) {
//don't check interface methods
}
@NbBundle.Messages({
"# {0} - function length in lines",
"# {1} - allowed lines per function declaration",
"FunctionLinesHintText=Method Length is {0} Lines ({1} allowed)"
})
private void checkBlock(Block block, OffsetRange warningRange) {
int countLines = block == null ? 0 : countLines(block);
if (countLines > maxAllowedLines) {
addHint(Bundle.FunctionLinesHintText(countLines, maxAllowedLines), warningRange);
}
}
}
public int getMaxAllowedLines(Preferences preferences) {
assert preferences != null;
return preferences.getInt(MAX_ALLOWED_FUNCTION_LINES, DEFAULT_MAX_ALLOWED_FUNCTION_LINES);
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("FunctionLinesHintDesc=Maximum allowed lines per function/method declaration.")
public String getDescription() {
return Bundle.FunctionLinesHintDesc();
}
@Override
@NbBundle.Messages("FunctionLinesHintDisp=Function (Method) Declaration")
public String getDisplayName() {
return Bundle.FunctionLinesHintDisp();
}
@Override
public JComponent getCustomizer(Preferences preferences) {
JComponent customizer = new FunctionLinesCustomizer(preferences, this);
setMaxAllowedLines(preferences, getMaxAllowedLines(preferences));
return customizer;
}
}
public static class ClassLinesHint extends TooManyLinesHint {
private static final String HINT_ID = "Class.Lines.Hint"; //NOI18N
private static final String MAX_ALLOWED_CLASS_LINES = "php.verification.max.allowed.class.lines"; //NOI18N
private static final int DEFAULT_MAX_ALLOWED_CLASS_LINES = 200;
private Preferences preferences;
@Override
CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument) {
return new ClassVisitor(this, fileObject, baseDocument, getMaxAllowedLines(preferences));
}
@Override
public void setPreferences(Preferences preferences) {
this.preferences = preferences;
}
public void setMaxAllowedLines(Preferences preferences, Integer value) {
assert preferences != null;
assert value != null;
preferences.putInt(MAX_ALLOWED_CLASS_LINES, value);
}
private static final class ClassVisitor extends CheckVisitor {
private final int maxAllowedLines;
public ClassVisitor(TooManyLinesHint linesHint, FileObject fileObject, BaseDocument baseDocument, int maxAllowedLines) {
super(linesHint, fileObject, baseDocument);
this.maxAllowedLines = maxAllowedLines;
}
@Override
public void visit(ClassDeclaration node) {
super.visit(node);
checkBlock(node.getBody(), new OffsetRange(node.getName().getStartOffset(), node.getName().getEndOffset()));
}
@NbBundle.Messages({
"# {0} - class length in lines",
"# {1} - allowed lines per class declaration",
"ClassLinesHintText=Class Length is {0} Lines ({1} allowed)"
})
private void checkBlock(Block block, OffsetRange warningRange) {
int countLines = countLines(block);
if (countLines > maxAllowedLines) {
addHint(Bundle.ClassLinesHintText(countLines, maxAllowedLines), warningRange);
}
}
}
public int getMaxAllowedLines(Preferences preferences) {
assert preferences != null;
return preferences.getInt(MAX_ALLOWED_CLASS_LINES, DEFAULT_MAX_ALLOWED_CLASS_LINES);
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("ClassLinesHintDesc=Maximum allowed lines per class declaration.")
public String getDescription() {
return Bundle.ClassLinesHintDesc();
}
@Override
@NbBundle.Messages("ClassLinesHintDisp=Class Declaration")
public String getDisplayName() {
return Bundle.ClassLinesHintDisp();
}
@Override
public JComponent getCustomizer(Preferences preferences) {
JComponent customizer = new ClassLinesCustomizer(preferences, this);
setMaxAllowedLines(preferences, getMaxAllowedLines(preferences));
return customizer;
}
}
public static class InterfaceLinesHint extends TooManyLinesHint {
private static final String HINT_ID = "Interface.Lines.Hint"; //NOI18N
private static final String MAX_ALLOWED_INTERFACE_LINES = "php.verification.max.allowed.interface.lines"; //NOI18N
private static final int DEFAULT_MAX_ALLOWED_INTERFACE_LINES = 100;
private Preferences preferences;
@Override
CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument) {
return new InterfaceVisitor(this, fileObject, baseDocument, getMaxAllowedLines(preferences));
}
@Override
public void setPreferences(Preferences preferences) {
this.preferences = preferences;
}
public void setMaxAllowedLines(Preferences preferences, Integer value) {
assert preferences != null;
assert value != null;
preferences.putInt(MAX_ALLOWED_INTERFACE_LINES, value);
}
private static final class InterfaceVisitor extends CheckVisitor {
private final int maxAllowedLines;
public InterfaceVisitor(TooManyLinesHint linesHint, FileObject fileObject, BaseDocument baseDocument, int maxAllowedLines) {
super(linesHint, fileObject, baseDocument);
this.maxAllowedLines = maxAllowedLines;
}
@Override
public void visit(InterfaceDeclaration node) {
super.visit(node);
checkBlock(node.getBody(), new OffsetRange(node.getName().getStartOffset(), node.getName().getEndOffset()));
}
@NbBundle.Messages({
"# {0} - interface length in lines",
"# {1} - allowed lines per interface declaration",
"InterfaceLinesHintText=Interface Length is {0} Lines ({1} allowed)"
})
private void checkBlock(Block block, OffsetRange warningRange) {
int countLines = countLines(block);
if (countLines > maxAllowedLines) {
addHint(Bundle.InterfaceLinesHintText(countLines, maxAllowedLines), warningRange);
}
}
}
public int getMaxAllowedLines(Preferences preferences) {
assert preferences != null;
return preferences.getInt(MAX_ALLOWED_INTERFACE_LINES, DEFAULT_MAX_ALLOWED_INTERFACE_LINES);
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("InterfaceLinesHintDesc=Maximum allowed lines per interface declaration.")
public String getDescription() {
return Bundle.InterfaceLinesHintDesc();
}
@Override
@NbBundle.Messages("InterfaceLinesHintDisp=Interface Declaration")
public String getDisplayName() {
return Bundle.InterfaceLinesHintDisp();
}
@Override
public JComponent getCustomizer(Preferences preferences) {
JComponent customizer = new InterfaceLinesCustomizer(preferences, this);
setMaxAllowedLines(preferences, getMaxAllowedLines(preferences));
return customizer;
}
}
public static class TraitLinesHint extends TooManyLinesHint {
private static final String HINT_ID = "Trait.Lines.Hint"; //NOI18N
private static final String MAX_ALLOWED_TRAIT_LINES = "php.verification.max.allowed.trait.lines"; //NOI18N
private static final int DEFAULT_MAX_ALLOWED_TRAIT_LINES = 200;
private Preferences preferences;
@Override
CheckVisitor createVisitor(FileObject fileObject, BaseDocument baseDocument) {
return new TraitVisitor(this, fileObject, baseDocument, getMaxAllowedLines(preferences));
}
@Override
public void setPreferences(Preferences preferences) {
this.preferences = preferences;
}
public void setMaxAllowedLines(Preferences preferences, Integer value) {
assert preferences != null;
assert value != null;
preferences.putInt(MAX_ALLOWED_TRAIT_LINES, value);
}
private static final class TraitVisitor extends CheckVisitor {
private final int maxAllowedLines;
public TraitVisitor(TooManyLinesHint linesHint, FileObject fileObject, BaseDocument baseDocument, int maxAllowedLines) {
super(linesHint, fileObject, baseDocument);
this.maxAllowedLines = maxAllowedLines;
}
@Override
public void visit(TraitDeclaration node) {
super.visit(node);
checkBlock(node.getBody(), new OffsetRange(node.getName().getStartOffset(), node.getName().getEndOffset()));
}
@NbBundle.Messages({
"# {0} - trait length in lines",
"# {1} - allowed lines per trait declaration",
"TraitLinesHintText=Trait Length is {0} Lines ({1} allowed)"
})
private void checkBlock(Block block, OffsetRange warningRange) {
int countLines = countLines(block);
if (countLines > maxAllowedLines) {
addHint(Bundle.TraitLinesHintText(countLines, maxAllowedLines), warningRange);
}
}
}
public int getMaxAllowedLines(Preferences preferences) {
assert preferences != null;
return preferences.getInt(MAX_ALLOWED_TRAIT_LINES, DEFAULT_MAX_ALLOWED_TRAIT_LINES);
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("TraitLinesHintDesc=Maximum allowed lines per trait declaration.")
public String getDescription() {
return Bundle.TraitLinesHintDesc();
}
@Override
@NbBundle.Messages("TraitLinesHintDisp=Trait Declaration")
public String getDisplayName() {
return Bundle.TraitLinesHintDisp();
}
@Override
public JComponent getCustomizer(Preferences preferences) {
JComponent customizer = new TraitLinesCustomizer(preferences, this);
setMaxAllowedLines(preferences, getMaxAllowedLines(preferences));
return customizer;
}
}
private abstract static class CheckVisitor extends DefaultVisitor {
private static final Logger LOGGER = Logger.getLogger(CheckVisitor.class.getName());
private final List<Hint> hints;
private final BaseDocument baseDocument;
private final FileObject fileObject;
private final TooManyLinesHint linesHint;
private final List<OffsetRange> commentRanges;
private CheckVisitor(TooManyLinesHint linesHint, FileObject fileObject, BaseDocument baseDocument) {
this.linesHint = linesHint;
this.fileObject = fileObject;
this.baseDocument = baseDocument;
hints = new ArrayList<>();
commentRanges = new ArrayList<>();
}
@Override
public void visit(Program node) {
for (Comment comment : node.getComments()) {
commentRanges.add(new OffsetRange(comment.getStartOffset(), comment.getEndOffset()));
}
super.visit(node);
}
protected int countLines(final Block block) {
final AtomicInteger result = new AtomicInteger(0);
baseDocument.render(new Runnable() {
@Override
public void run() {
result.set(countLinesUnderReadLock(block));
}
});
return result.get();
}
private int countLinesUnderReadLock(Block block) {
int result = 0;
try {
result = tryCountLines(block);
} catch (BadLocationException ex) {
// see issue 227687 and #172881
LOGGER.log(Level.FINE, null, ex);
}
return result;
}
private int tryCountLines(Block block) throws BadLocationException {
int searchOffset = block.getStartOffset() + 1;
int firstNonWhiteFwd = LineDocumentUtils.getNextNonWhitespace(baseDocument, searchOffset);
int startLineOffset = LineDocumentUtils.getLineIndex(baseDocument, firstNonWhiteFwd == -1 ? searchOffset : firstNonWhiteFwd);
int endLineOffset = LineDocumentUtils.getLineIndex(baseDocument, LineDocumentUtils.getPreviousNonWhitespace(baseDocument, block.getEndOffset()));
return countLinesBetweenLineOffsets(startLineOffset, endLineOffset);
}
private int countLinesBetweenLineOffsets(int startLineOffset, int endLineOffset) throws BadLocationException {
int result = 0;
for (int lineOffset = startLineOffset; lineOffset < endLineOffset; lineOffset++) {
int rowStartFromLineOffset = LineDocumentUtils.getLineStartFromIndex(baseDocument, lineOffset);
if (!LineDocumentUtils.isLineWhitespace(baseDocument, rowStartFromLineOffset) && !isJustCommentOnLine(rowStartFromLineOffset)) {
result++;
}
}
return result;
}
private boolean isJustCommentOnLine(int rowStartOffset) throws BadLocationException {
boolean result = false;
int rowFirstNonWhite = LineDocumentUtils.getLineFirstNonWhitespace(baseDocument, rowStartOffset);
int rowLastNonWhite = LineDocumentUtils.getLineLastNonWhitespace(baseDocument, rowStartOffset);
for (OffsetRange commentRange : commentRanges) {
if (commentRange.containsInclusive(rowFirstNonWhite) && commentRange.containsInclusive(rowLastNonWhite)) {
result = true;
break;
}
}
return result;
}
protected void addHint(String description, OffsetRange warningRange) {
if (linesHint.showHint(warningRange, baseDocument)) {
hints.add(new Hint(linesHint, description, fileObject, warningRange, null, 500));
}
}
public List<Hint> getHints() {
return hints;
}
}
}