blob: d1d5c0f44d88cc485b7b715182628506f3074678 [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.Collections;
import java.util.List;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.lexer.TokenUtilities;
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.lexer.LexUtilities;
import org.netbeans.modules.php.editor.lexer.PHPTokenId;
import org.netbeans.modules.php.editor.parser.PHPParseResult;
import org.openide.filesystems.FileObject;
import org.openide.util.NbBundle;
/**
*
* @author Ondrej Brejla <obrejla@netbeans.org>
*/
public class UnnecessaryClosingDelimiterHint extends HintRule {
private static final String HINT_ID = "unnecessary.closing.delimiter.hint";
private FileObject fileObject;
private BaseDocument baseDocument;
@Override
public void invoke(PHPRuleContext context, List<Hint> result) {
PHPParseResult phpParseResult = (PHPParseResult) context.parserResult;
if (phpParseResult.getProgram() != null) {
fileObject = phpParseResult.getSnapshot().getSource().getFileObject();
baseDocument = context.doc;
if (fileObject != null) {
if (CancelSupport.getDefault().isCancelled()) {
return;
}
TokenHierarchy<?> th = phpParseResult.getSnapshot().getTokenHierarchy();
if (th != null) {
int startOffset = phpParseResult.getProgram().getStartOffset();
TokenSequence<PHPTokenId> ts = LexUtilities.getPHPTokenSequence(th, startOffset);
if (ts != null) {
ts.move(startOffset);
CloseTagWrapper closeTagWrapper = createCloseTagWrapper(ts);
if (closeTagWrapper.shouldBeRemoved()) {
closeTagWrapper.createHint(result);
}
}
}
}
}
}
private CloseTagWrapper createCloseTagWrapper(TokenSequence<PHPTokenId> ts) {
CloseTagWrapper result = CloseTagWrapper.NONE;
boolean inOpenTagWithEcho = false;
while (ts.moveNext()) {
Token<PHPTokenId> token = ts.token();
if (token != null) {
PHPTokenId id = token.id();
switch (id) {
case T_OPEN_TAG_WITH_ECHO:
inOpenTagWithEcho = true;
break;
case PHP_CLOSETAG:
result = new CloseTagWrapperImpl(ts.offset(), inOpenTagWithEcho);
break;
case T_INLINE_HTML:
result.setHtmlPart(token);
break;
default:
result = CloseTagWrapper.NONE;
}
}
}
return result;
}
private interface CloseTagWrapper {
CloseTagWrapper NONE = new CloseTagWrapper() {
@Override
public void setHtmlPart(Token<PHPTokenId> inlineHtmlTag) {
}
@Override
public boolean shouldBeRemoved() {
return false;
}
@Override
public void createHint(List<Hint> result) {
}
};
void setHtmlPart(Token<PHPTokenId> inlineHtmlTag);
boolean shouldBeRemoved();
void createHint(List<Hint> result);
}
private final class CloseTagWrapperImpl implements CloseTagWrapper {
private static final String CLOSING_TAG = "?>"; //NOI18N
private final int closeTagOffset;
private final boolean startsWithOpenTagWithEcho;
private Token<PHPTokenId> inlineHtmlTag;
private CloseTagWrapperImpl(int closeTagOffset, boolean startsWithOpenTagWithEcho) {
this.closeTagOffset = closeTagOffset;
this.startsWithOpenTagWithEcho = startsWithOpenTagWithEcho;
}
@Override
public void setHtmlPart(Token<PHPTokenId> inlineHtmlTag) {
this.inlineHtmlTag = inlineHtmlTag;
}
@Override
public boolean shouldBeRemoved() {
return !startsWithOpenTagWithEcho && (inlineHtmlTag == null || TokenUtilities.trim(inlineHtmlTag.text()).length() == 0);
}
@NbBundle.Messages("UnnecessaryClosingDelimiterHintText=Unnecessary Closing Delimiter")
@Override
public void createHint(List<Hint> result) {
OffsetRange hintRange = new OffsetRange(closeTagOffset, closeTagOffset + CLOSING_TAG.length());
result.add(new Hint(
UnnecessaryClosingDelimiterHint.this,
Bundle.UnnecessaryClosingDelimiterHintText(),
fileObject,
hintRange,
Collections.<HintFix>singletonList(new Fix(hintRange, baseDocument)),
500));
}
}
private static final class Fix implements HintFix {
private final OffsetRange hintRange;
private final BaseDocument baseDocument;
public Fix(OffsetRange hintRange, BaseDocument baseDocument) {
this.hintRange = hintRange;
this.baseDocument = baseDocument;
}
@Override
@NbBundle.Messages("UnnecessaryClosingDelimiterHintFix=Remove Closing Delimiter")
public String getDescription() {
return Bundle.UnnecessaryClosingDelimiterHintFix();
}
@Override
public void implement() throws Exception {
EditList editList = new EditList(baseDocument);
editList.replace(hintRange.getStart(), hintRange.getLength(), "", true, 0);
editList.apply();
}
@Override
public boolean isSafe() {
return true;
}
@Override
public boolean isInteractive() {
return false;
}
}
@Override
public String getId() {
return HINT_ID;
}
@Override
@NbBundle.Messages("UnnecessaryClosingDelimiterHintDesc=It is a good practise to omit closing PHP delimiter "
+ "at the end of file. It's just a source of \"Headers already sent\" errors.")
public String getDescription() {
return Bundle.UnnecessaryClosingDelimiterHintDesc();
}
@Override
@NbBundle.Messages("UnnecessaryClosingDelimiterHintDisp=Unnecessary Closing Delimiter")
public String getDisplayName() {
return Bundle.UnnecessaryClosingDelimiterHintDisp();
}
}