blob: e59674298e344ebae74a3caf73ce54203c644358 [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.javascript2.editor.parser;
import com.oracle.js.parser.Token;
import com.oracle.js.parser.TokenType;
import com.oracle.js.parser.ErrorManager;
import com.oracle.js.parser.ParserException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.antlr.v4.runtime.ANTLRErrorListener;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.editor.document.LineDocument;
import org.netbeans.api.editor.document.LineDocumentUtils;
import org.netbeans.api.lexer.Language;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.api.Error;
import org.netbeans.modules.csl.api.Severity;
import org.netbeans.modules.css.lib.api.FilterableError;
import org.netbeans.modules.javascript2.editor.embedding.JsEmbeddingProvider;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.modules.parsing.api.Snapshot;
import org.openide.filesystems.FileObject;
/**
*
* @author Petr Hejl, Petr Pisl
*/
public class JsErrorManager extends ErrorManager implements ANTLRErrorListener {
private static final Logger LOGGER = Logger.getLogger(JsErrorManager.class.getName());
private static final int MAX_MESSAGE_LENGTH = 100;
private static final boolean SHOW_BADGES_EMBEDDED = Boolean.getBoolean(JsErrorManager.class.getName() + ".showBadgesEmbedded");;
private static final Comparator<SimpleError> POSITION_COMPARATOR = (SimpleError o1, SimpleError o2) -> {
if (o1.getStartPosition() < o2.getStartPosition()) {
return -1;
}
if (o1.getStartPosition() > o2.getStartPosition()) {
return 1;
}
return 0;
};
// message pattern is for example "index.html:2:16 Exepcted ;"
private static final Pattern ERROR_MESSAGE_PATTERN = Pattern.compile(".*:\\d+:\\d+ (.*)", Pattern.DOTALL); // NOI18N
// used to replace pointers from mesage such as
// Expected ( but found else
// else
// ^
// with this pattern we replace last two lines and related new lines
private static final Pattern REPLACE_POINTER_PATTERN = Pattern.compile("(\\n)+.*\\n\\s*\\^\\s*"); // NOI18N
/** Keyword from the error.message which identifies missing char in the JS source. */
private static final String EXPECTED = "Expected"; //NOI18N
private final Snapshot snapshot;
private final Language<JsTokenId> language;
private List<ParserError> parserErrors;
private List<JsParserError> convertedErrors;
private static final Map<String, JsTokenId> JS_TEXT_TOKENS = new HashMap<>();
static {
for (JsTokenId jsTokenId : JsTokenId.values()) {
if (jsTokenId.fixedText() != null) {
JS_TEXT_TOKENS.put(jsTokenId.fixedText(), jsTokenId);
}
}
}
public JsErrorManager(Snapshot snapshot, Language<JsTokenId> language) {
this.snapshot = snapshot;
this.language = language;
}
Error getMissingCurlyError() {
if (parserErrors == null) {
return null;
}
final FileObject file = snapshot != null ? snapshot.getSource().getFileObject() : null;
Collection<FilterableError.SetFilterAction> enableFilterAction = file != null
? ParsingErrorFilter.getEnableFilterAction(file)
: Collections.emptyList();
FilterableError.SetFilterAction disableFilterAction = file != null
? ParsingErrorFilter.getDisableFilterAction(file)
: null;
for (ParserError error : parserErrors) {
if (error.message != null
&& (error.message.contains("Expected }") || error.message.contains("but found }"))) { // NOI18N
return new JsParserError(error.toSimpleError(snapshot, language),
snapshot != null ? snapshot.getSource().getFileObject() : null,
Severity.ERROR, null, false, false,
enableFilterAction, disableFilterAction);
}
}
return null;
}
Error getMissingSemicolonError() {
if (parserErrors == null) {
return null;
}
final FileObject file = snapshot != null ? snapshot.getSource().getFileObject() : null;
Collection<FilterableError.SetFilterAction> enableFilterAction = file != null
? ParsingErrorFilter.getEnableFilterAction(file)
: Collections.emptyList();
FilterableError.SetFilterAction disableFilterAction = file != null
? ParsingErrorFilter.getDisableFilterAction(file)
: null;
for (ParserError error : parserErrors) {
if (error.message != null
&& error.message.contains("Expected ;")) { // NOI18N
return new JsParserError(error.toSimpleError(snapshot, language),
snapshot != null ? snapshot.getSource().getFileObject() : null,
Severity.ERROR, null, false, false,
enableFilterAction, disableFilterAction);
}
}
return null;
}
public boolean isEmpty() {
return parserErrors == null;
}
@Override
public void error(ParserException e) {
addParserError(new NashornParserError(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e.getToken()));
}
@Override
public void error(String message) {
LOGGER.log(Level.FINE, "Error {0}", message);
addParserError(new NashornParserError(message));
}
@Override
public void warning(ParserException e) {
LOGGER.log(Level.FINE, null, e);
}
@Override
public void warning(String message) {
LOGGER.log(Level.FINE, "Warning {0}", message);
}
public List<? extends FilterableError> getErrors() {
if (convertedErrors == null) {
if (parserErrors == null) {
convertedErrors = Collections.emptyList();
} else {
ArrayList<SimpleError> errors = new ArrayList<>(parserErrors.size());
for (ParserError error : parserErrors) {
errors.add(error.toSimpleError(snapshot, language));
}
Collections.sort(errors, POSITION_COMPARATOR);
convertedErrors = convert(snapshot, errors);
}
}
return Collections.unmodifiableList(convertedErrors);
}
JsErrorManager fillErrors(JsErrorManager original) {
assert this.snapshot == original.snapshot : this.snapshot + ":" + original.snapshot;
assert this.language == original.language : this.language + ":" + original.language;
if (original.parserErrors != null) {
this.parserErrors = new ArrayList<>(original.parserErrors);
} else {
this.parserErrors = null;
}
this.convertedErrors = null;
return this;
}
private void addParserError(ParserError error) {
convertedErrors = null;
if (parserErrors == null) {
parserErrors = new ArrayList<>();
}
parserErrors.add(error);
}
private static List<JsParserError> convert(Snapshot snapshot, List<SimpleError> errors) {
// basically we are solwing showExplorerBadge attribute here
List<JsParserError> ret = new ArrayList<>(errors.size());
final FileObject file = snapshot != null ? snapshot.getSource().getFileObject() : null;
Collection<FilterableError.SetFilterAction> enableFilterAction = file != null
? ParsingErrorFilter.getEnableFilterAction(file)
: Collections.emptyList();
FilterableError.SetFilterAction disableFilterAction = file != null
? ParsingErrorFilter.getDisableFilterAction(file)
: null;
if (snapshot != null && BaseParserResult.isEmbedded(snapshot)) {
int nextCorrect = -1;
boolean afterGeneratedIdentifier = false;
for (SimpleError error : errors) {
boolean showInEditor = true;
// if the error is in embedded code we ignore it
// as we don't know what the other language will add
int pos = snapshot.getOriginalOffset(error.getStartPosition());
if (pos >= 0 && nextCorrect <= error.getStartPosition()
&& !JsEmbeddingProvider.containsGeneratedIdentifier(error.getMessage())) {
TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(
snapshot, error.getStartPosition());
if (ts != null && ts.movePrevious()) {
// check also a previous token - is it generated ?
org.netbeans.api.lexer.Token<? extends JsTokenId> token =
LexUtilities.findPreviousNonWsNonComment(ts);
if (JsEmbeddingProvider.containsGeneratedIdentifier(token.text().toString())) {
// usually we may expect a group of errors
// so we disable them until next } .... \n
nextCorrect = findNextCorrectOffset(ts, error.getStartPosition());
showInEditor = false;
afterGeneratedIdentifier = true;
} else if (afterGeneratedIdentifier && error.getMessage().contains(EXPECTED)) {
// errors after generated identifiers can display farther - see issue #229985
String expected = getExpected(error.getMessage());
if ("eof".equals(expected)) { //NOI18N
// unexpected end of script, probably missing at some earlier place : ; } etc.
showInEditor = false;
} else {
JsTokenId expectedToken = getJsTokenFromString(expected);
ts.movePrevious();
org.netbeans.api.lexer.Token<? extends JsTokenId> previousNonWsToken = LexUtilities.findPreviousNonWsNonComment(ts);
if (expectedToken != null && expectedToken == previousNonWsToken.id()) {
// char is available, doesn't show the error
showInEditor = false;
}
}
}
}
} else {
showInEditor = false;
}
ret.add(new JsParserError(error, file, Severity.ERROR, null, SHOW_BADGES_EMBEDDED, showInEditor, enableFilterAction, disableFilterAction));
}
} else {
for (SimpleError error : errors) {
ret.add(new JsParserError(error, file, Severity.ERROR, null, true, true, enableFilterAction, disableFilterAction));
}
}
return ret;
}
private static String getExpected(String errorMessage) {
int expectedIndex = errorMessage.indexOf(EXPECTED);
String afterExpected = errorMessage.substring(expectedIndex + 9);
int indexOfSpace = afterExpected.indexOf(" "); //NOI18N
return (indexOfSpace != -1) ? afterExpected.substring(0, indexOfSpace) : afterExpected;
}
public static JsTokenId getJsTokenFromString(String name) {
return JS_TEXT_TOKENS.get(name);
}
private static int findNextCorrectOffset(TokenSequence<? extends JsTokenId> ts, int offset) {
ts.move(offset);
if (ts.moveNext()) {
LexUtilities.findNextIncluding(ts, Collections.singletonList(JsTokenId.BRACKET_LEFT_CURLY));
LexUtilities.findNextIncluding(ts, Collections.singletonList(JsTokenId.EOL));
}
return ts.offset();
}
//--- Antlr4 ---
@Override
public void syntaxError(
final Recognizer<?, ?> recognizer,
final Object offendingSymbol,
final int line,
final int charPositionInLine,
final String msg,
final RecognitionException e) {
if (recognizer instanceof Parser) {
//Recognizer can be either Parser or Lexer
List<String> stack = ((Parser) recognizer).getRuleInvocationStack();
Collections.reverse(stack);
}
addParserError(new AntlrParserError(msg, line, charPositionInLine, offendingSymbol));
}
@Override
public void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean bln, BitSet bitset, ATNConfigSet atncs) {
//Not important
}
@Override
public void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitset, ATNConfigSet atncs) {
//Not important
}
@Override
public void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atncs) {
//Not important
}
static final class SimpleError {
private final String message;
private final boolean lineError;
private final int startPosition;
private final int endPosition;
private SimpleError(
final String message,
final boolean lineError,
final int startPosition,
final int endPosition) {
this.message = message;
this.lineError = lineError;
this.startPosition = startPosition;
this.endPosition = endPosition;
}
public String getMessage() {
return message;
}
public boolean isLineError() {
return lineError;
}
public int getStartPosition() {
return startPosition;
}
public int getEndPosition() {
return endPosition;
}
}
private abstract static class ParserError {
final String message;
final int line;
final int column;
ParserError(String message, int line, int column) {
if (message.length() > MAX_MESSAGE_LENGTH) {
int index = message.indexOf('\n', MAX_MESSAGE_LENGTH);
this.message = message.substring(0, (index < MAX_MESSAGE_LENGTH && index > 0) ? index : MAX_MESSAGE_LENGTH);
LOGGER.log(Level.FINE, "Too long error message {0}", message);
} else {
this.message = message;
}
this.line = line;
this.column = column;
}
abstract SimpleError toSimpleError(@NonNull Snapshot snapshot, @NonNull Language<JsTokenId> language);
}
private static final class NashornParserError extends ParserError {
final long token;
NashornParserError(String message, int line, int column, long token) {
super(message, line, column);
this.token = token;
}
NashornParserError(String message, long token) {
this(message, -1, -1, token);
}
NashornParserError(String message) {
this(message, -1, -1, -1);
}
@Override
SimpleError toSimpleError(
@NonNull final Snapshot snapshot,
@NonNull final Language<JsTokenId> language) {
String message = this.message;
int offset = -1;
Matcher matcher = ERROR_MESSAGE_PATTERN.matcher(message);
if (matcher.matches()) {
message = matcher.group(1);
}
message = REPLACE_POINTER_PATTERN.matcher(message).replaceAll(""); // NOI18N
if (this.token > 0) {
offset = Token.descPosition(this.token);
if (Token.descType(this.token) == TokenType.EOF
&& snapshot.getOriginalOffset(offset) == -1) {
int realOffset = -1;
TokenSequence<? extends JsTokenId> ts =
LexUtilities.getPositionedSequence(snapshot, offset, language);
while (ts.movePrevious()) {
if (snapshot.getOriginalOffset(ts.offset()) > 0) {
realOffset = ts.offset() + ts.token().length() - 1;
break;
}
}
if (realOffset > 0) {
offset = realOffset;
}
}
} else if (this.line == -1 && this.column == -1) {
// is this still used ?
String parts[] = this.message.split(":");
// if (parts.length > 4) {
// message = parts[4];
// int index = message.indexOf('\n');
// if (index > 0) {
// message = message.substring(0, index);
// }
//
// }
if (parts.length > 3) {
try {
offset = Integer.parseInt(parts[3]);
} catch (NumberFormatException nfe) {
// do nothing
}
}
}
return new SimpleError(message, true, offset, offset+1);
}
}
private static final class AntlrParserError extends ParserError {
final Object token;
public AntlrParserError(String message, int line, int column, Object token) {
super(message, line, column);
this.token = token;
}
@Override
SimpleError toSimpleError(Snapshot snapshot, Language<JsTokenId> language) {
String message = this.message;
LineDocument doc = (LineDocument)snapshot.getSource().getDocument(false);
if (doc == null) {
LOGGER.log(Level.WARNING, "No document found");
return new SimpleError(message, false, 0, 0);
}
int lineOffset = LineDocumentUtils.getLineStartFromIndex((LineDocument)snapshot.getSource().getDocument(false), this.line - 1);
int offset = lineOffset + this.column;
if (offset > -1 && offset < snapshot.getText().length()
&& snapshot.getOriginalOffset(offset) == -1) {
int realOffset = -1;
TokenSequence<? extends JsTokenId> ts
= LexUtilities.getPositionedSequence(snapshot, offset, language);
while (ts.movePrevious()) {
if (snapshot.getOriginalOffset(ts.offset()) > 0) {
realOffset = ts.offset() + ts.token().length() - 1;
break;
}
}
if (realOffset > 0) {
offset = realOffset;
}
}
int endOffset = -1;
if (offset >= 0) {
endOffset = offset + 1;
if (token instanceof org.antlr.v4.runtime.Token) {
org.antlr.v4.runtime.Token t = (org.antlr.v4.runtime.Token)token;
int len = t.getStopIndex() - t.getStartIndex();
if (len > 0) {
endOffset = offset + len + 1;
}
}
}
return new SimpleError(
message,
false,
offset,
endOffset);
}
}
}