blob: 75daec54e3277618f131646e31fae10d5297cf4d [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.yaml.core.ast;
import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import static org.eclipse.statet.ltk.ast.core.AstNode.NA_OFFSET;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS1_SYNTAX_MISSING_INDICATOR;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS2_SYNTAX_CHAR_INVALID;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS2_SYNTAX_COLLECTION_NOT_CLOSED;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS2_SYNTAX_ESCAPE_INVALID;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS2_SYNTAX_TOKEN_NOT_CLOSED;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS2_SYNTAX_TOKEN_UNEXPECTED;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS2_SYNTAX_TOKEN_UNKNOWN;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS3_FLOW_MAP;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS3_FLOW_SEQ;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS3_MAP_KEY;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS3_MAP_VALUE;
import static org.eclipse.statet.yaml.core.ast.YamlAstStatusConstants.STATUS3_SEQ_ENTRY;
import java.util.ArrayList;
import java.util.List;
import org.yaml.snakeyaml.error.Mark;
import org.yaml.snakeyaml.tokens.AliasToken;
import org.yaml.snakeyaml.tokens.ScalarToken;
import org.yaml.snakeyaml.tokens.TagToken;
import org.yaml.snakeyaml.tokens.TagTuple;
import org.yaml.snakeyaml.tokens.Token;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.internal.yaml.snakeyaml.scanner.ScannerConstants;
import org.eclipse.statet.internal.yaml.snakeyaml.scanner.ScannerImpl;
import org.eclipse.statet.internal.yaml.snakeyaml.scanner.StreamReader;
import org.eclipse.statet.ltk.ast.core.AstNode;
import org.eclipse.statet.ltk.ast.core.StatusDetail;
@NonNullByDefault
public class YamlParser {
private static class ProblemInfo {
//private final byte context;
private final @Nullable Mark contextMark;
private final byte problem;
private final @Nullable Mark problemMark;
private final @Nullable String problemText;
public ProblemInfo(final byte context, final @Nullable Mark contextMark,
final byte problem, final @Nullable Mark problemMark, final @Nullable String problemText) {
//this.context= context;
this.contextMark= contextMark;
this.problem= problem;
this.problemMark= problemMark;
this.problemText= problemText;
}
}
private final ScannerImpl scanner= new ScannerImpl(new StreamReader(""), true, true, false) { //$NON-NLS-1$
@Override
protected void handleComment(final int startIndex, final int endIndex) {
}
@Override
protected void handleSyntaxProblem(final byte context, final @Nullable Mark contextMark,
final byte problem, final @Nullable Mark problemMark, final @Nullable String problemArg1, final @Nullable String problemArg2) {
YamlParser.this.handleScannerProblem(new ProblemInfo(context, contextMark,
problem, problemMark, problemArg1 ));
}
};
private @Nullable YamlAstNode currentNode;
private int depth;
private final List<List<YamlAstNode>> childrenStack= new ArrayList<>();
private final List<ProblemInfo> problemInfos= new ArrayList<>();
public YamlParser() {
}
public void setScalarText(final boolean create) {
this.scanner.setCreateScalarText(create);
}
public SourceComponent parse(final String text, final @Nullable AstNode parent, final int offset) {
try {
this.depth= -1;
this.scanner.reset(text, offset);
final SourceComponent sourceComponent= new SourceComponent(parent, offset, text.length());
enterNode(sourceComponent);
processTokens();
while (this.depth >= 0) {
exit();
}
return sourceComponent;
}
finally {
while (this.depth >= 0) {
final List<YamlAstNode> list= this.childrenStack.get(this.depth);
list.clear();
this.depth--;
}
this.problemInfos.clear();
}
}
public SourceComponent parse(final String text) {
return parse(text, null, 0);
}
private void addChildTerm(final YamlAstNode node) {
addChild(node);
checkExit();
}
private void addChild(final YamlAstNode node) {
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
if (currentNode.getNodeType() == NodeType.MAP_ENTRY) {
final Tuple entry= (Tuple) currentNode;
if (entry.keyNode == null && entry.valueIndicatorOffset == NA_OFFSET) {
entry.keyNode= node;
}
else {
entry.valueNode= node;
}
}
else {
final List<YamlAstNode> children= this.childrenStack.get(this.depth);
children.add(node);
if (this.depth == 0) {
clearProblems(node.getStartOffset());
}
}
}
private void finish(final int endOffset) {
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
switch (currentNode.getNodeType()) {
case MAP_ENTRY: {
final Tuple entry= (Tuple) currentNode;
if (entry.keyNode == null) {
entry.keyNode= new Dummy(0, entry, // empty node
(entry.keyIndicatorOffset != NA_OFFSET) ?
entry.keyIndicatorOffset : entry.startOffset );
}
if (entry.status == 0 && entry.valueIndicatorOffset == Integer.MAX_VALUE) {
entry.status= STATUS1_SYNTAX_MISSING_INDICATOR | STATUS3_MAP_VALUE;
}
if (entry.valueNode == null) {
entry.valueNode= new Dummy(0, entry, // empty node
(entry.valueIndicatorOffset != NA_OFFSET) ?
entry.valueIndicatorOffset : entry.keyNode.endOffset );
}
final int min= entry.valueNode.endOffset;
if (entry.endOffset < min) {
entry.endOffset= min;
}
return;
}
case MAP: {
final Collection collection= (Collection) currentNode;
switch (collection.getOperator()) {
case '[':
if (collection.getCloseIndicatorOffset() == NA_OFFSET) {
collection.status= STATUS2_SYNTAX_COLLECTION_NOT_CLOSED | STATUS3_FLOW_SEQ;
}
break;
case '{':
if (collection.getCloseIndicatorOffset() == NA_OFFSET) {
collection.status= STATUS2_SYNTAX_COLLECTION_NOT_CLOSED | STATUS3_FLOW_MAP;
}
break;
}
break; // -> NContainer
}
default:
break; // -> NContainer
}
{ final NContainer container= (NContainer) currentNode;
if (endOffset != NA_OFFSET) {
container.endOffset= endOffset;
}
final List<YamlAstNode> children= this.childrenStack.get(this.depth);
if (!children.isEmpty()) {
container.children= children.toArray(new YamlAstNode[children.size()]);
children.clear();
final int min= container.children[container.children.length - 1].getEndOffset();
if (container.endOffset < min) {
container.endOffset= min;
}
}
}
}
private void enterNode(final NContainer node) {
if (this.depth >= 0) {
addChild(node);
}
this.depth++;
this.currentNode= node;
while (this.depth >= this.childrenStack.size()) {
this.childrenStack.add(new ArrayList<YamlAstNode>());
}
}
private void enterNode(final Tuple node) {
addChild(node);
this.depth++;
this.currentNode= node;
}
private boolean exitTo(final Class<?> type1) {
while (this.depth > 1) {
if (this.currentNode.getClass() == type1) {
return true;
}
exit();
}
return false;
}
private boolean exitTo(final Class<?> type1, final Class<?> type2) {
while (this.depth > 1) {
if (this.currentNode.getClass() == type1 || this.currentNode.getClass() == type2) {
return true;
}
exit();
}
return false;
}
private boolean exitTo1(final Class<?> type1) {
while (this.depth > 1) {
if (this.currentNode.getClass() == type1) {
return true;
}
if (this.currentNode.getNodeType() == NodeType.MAP_ENTRY) {
exit();
}
break;
}
int checkDepth= this.depth - 1;
YamlAstNode node= this.currentNode.getYamlParent();
while (checkDepth > 1 && node != null) {
if (node.getClass() == type1) {
while (this.depth > checkDepth) {
exit();
}
return true;
}
checkDepth--;
node= node.getYamlParent();
}
return false;
}
private boolean exitTo1(final Class<?> type1, final Class<?> type2) {
while (this.depth > 1) {
if (this.currentNode.getClass() == type1 || this.currentNode.getClass() == type2) {
return true;
}
if (this.currentNode.getNodeType() == NodeType.MAP_ENTRY) {
exit();
}
break;
}
int checkDepth= this.depth - 1;
YamlAstNode node= this.currentNode.getYamlParent();
while (checkDepth > 1 && node != null) {
if (node.getClass() == type1 || node.getClass() == type2) {
while (this.depth > checkDepth) {
exit();
}
return true;
}
checkDepth--;
node= node.getYamlParent();
}
return false;
}
private boolean exitTo(final NodeType type1) {
while (this.depth > 1) {
if (this.currentNode.getNodeType() == type1) {
return true;
}
exit();
}
return false;
}
private boolean exitTo(final NodeType type1, final NodeType type2) {
while (this.depth > 1) {
if (this.currentNode.getNodeType() == type1 || this.currentNode.getNodeType() == type2) {
return true;
}
exit();
}
return false;
}
private void exitToSourceComponent(final int offset) {
while (this.depth > 1) {
exit();
}
if (this.depth > 0) {
exit(offset);
}
}
private void exit(final int offset) {
finish(offset);
this.currentNode= this.currentNode.yamlParent;
this.depth--;
checkExit();
}
private void exit() {
finish(NA_OFFSET);
this.currentNode= this.currentNode.yamlParent;
this.depth--;
checkExit();
}
private void checkExit() {
if (this.depth > 0 && this.currentNode.getNodeType() == NodeType.MAP_ENTRY
&& ((Tuple) this.currentNode).valueNode != null) {
exit();
}
}
private void processTokens() {
while (true) {
final Token token= this.scanner.nextToken();
if (token == null) {
break;
}
switch (token.getTokenId()) {
case StreamStart:
case StreamEnd:
continue;
case Directive: {
exitToSourceComponent(token.getStartMark().getIndex());
final Directive node= new Directive((SourceComponent) this.currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
addChildTerm(node);
checkForProblem(token.getStartMark(), node);
continue;
}
case DocumentStart: {
exitToSourceComponent(token.getStartMark().getIndex());
final Instruction.DocStart node= new Instruction.DocStart((SourceComponent) this.currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
addChildTerm(node);
checkForProblem(token.getStartMark(), node);
enterNode(new DocContent((SourceComponent) this.currentNode,
token.getStartMark().getIndex() ));
continue;
}
case DocumentEnd: {
exitToSourceComponent(token.getStartMark().getIndex());
final Instruction.DocEnd node= new Instruction.DocEnd((SourceComponent) this.currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
addChildTerm(node);
checkForProblem(token.getStartMark(), node);
continue;
}
case BlockSequenceStart: {
final Collection.BlockSeq node= new Collection.BlockSeq(this.currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
enterNode(node);
checkForProblem(token.getStartMark(), node);
continue;
}
case BlockMappingStart: {
final Collection.BlockMap node= new Collection.BlockMap(this.currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
enterNode(node);
checkForProblem(token.getStartMark(), node);
continue;
}
case BlockEnd: {
final boolean found= exitTo(Collection.BlockSeq.class, Collection.BlockMap.class);
if (found) {
exit(token.getEndMark().getIndex());
}
continue;
}
case FlowSequenceStart: {
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Collection.FlowCollection node= new Collection.FlowSeq(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
enterNode(node);
checkForProblem(token.getStartMark(), node);
continue;
}
case FlowSequenceEnd: {
final boolean found= exitTo(Collection.FlowSeq.class);
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Collection.FlowSeq collection;
if (found) {
collection= (Collection.FlowSeq) currentNode;
collection.closeIndicatorOffset= token.getStartMark().getIndex();
exit(token.getEndMark().getIndex());
}
else {
addChildTerm(new Dummy(STATUS2_SYNTAX_TOKEN_UNEXPECTED, currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() ));
}
continue;
}
case FlowMappingStart: {
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Collection.FlowCollection node= new Collection.FlowMap(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
enterNode(node);
checkForProblem(token.getStartMark(), node);
continue;
}
case FlowMappingEnd: {
final boolean found= exitTo(Collection.FlowMap.class);
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Collection.FlowMap node;
if (found) {
node= (Collection.FlowMap) currentNode;
node.closeIndicatorOffset= token.getStartMark().getIndex();
exit(token.getEndMark().getIndex());
}
else {
addChildTerm(new Dummy(STATUS2_SYNTAX_TOKEN_UNEXPECTED, currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() ));
}
continue;
}
case BlockEntry: {
final boolean found= exitTo1(Collection.BlockSeq.class);
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Collection.BlockSeq node;
if (found) {
node= (Collection.BlockSeq) currentNode;
}
else {
node= new Collection.BlockSeq(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
enterNode(node);
checkForProblem(token.getStartMark(), node);
}
continue;
}
case FlowEntry: {
final boolean found= exitTo1(Collection.FlowSeq.class, Collection.FlowMap.class);
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
if (!found) {
addChildTerm(new Dummy(STATUS2_SYNTAX_TOKEN_UNEXPECTED | STATUS3_SEQ_ENTRY,
currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() ));
}
continue;
}
case Anchor: {
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Label node= new Label.Anchor(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex(),
((AliasToken) token).getValue() );
addChildTerm(node);
checkForProblem(token.getStartMark(), node);
continue;
}
case Alias: {
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Label node= new Label.Reference(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex(),
((AliasToken) token).getValue() );
addChildTerm(node);
checkForProblem(token.getStartMark(), node);
continue;
}
case Key: {
final boolean found= exitTo(NodeType.MAP);
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Tuple node= new Tuple(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
if (!found) {
node.status|= STATUS2_SYNTAX_TOKEN_UNEXPECTED | STATUS3_MAP_KEY;
}
enterNode(node);
checkForProblem(token.getStartMark(), node);
if (node.getLength() > 0) { // explicite
node.keyIndicatorOffset= node.startOffset;
}
continue;
}
case Value: {
final boolean found= exitTo(NodeType.MAP_ENTRY, NodeType.MAP);
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Tuple node;
if (currentNode.getNodeType() == NodeType.MAP_ENTRY) {
node= (Tuple) currentNode;
}
else {
node= new Tuple(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex() );
if (!found) {
node.status|= STATUS2_SYNTAX_TOKEN_UNEXPECTED | STATUS3_MAP_VALUE;
}
enterNode(node);
}
checkForProblem(token.getStartMark(), node);
node.valueIndicatorOffset= token.getStartMark().getIndex();
node.endOffset= token.getEndMark().getIndex();
continue;
}
case Tag: {
final TagTuple tagTuple= ((TagToken) token).getValue();
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Tag node= new Tag(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex(),
tagTuple.getHandle(), tagTuple.getSuffix() );
addChildTerm(node);
checkForProblem(token.getStartMark(), node);
continue;
}
case Scalar: {
final ScalarToken scalarToken= (ScalarToken) token;
final YamlAstNode currentNode= nonNullAssert(this.currentNode);
final Scalar node;
switch (scalarToken.getStyle()) {
case DOUBLE_QUOTED:
node= new Scalar.DQuoted(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex(),
scalarToken.getValue() );
break;
case SINGLE_QUOTED:
node= new Scalar.SQuoated(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex(),
scalarToken.getValue() );
break;
default:
node= new Scalar.Plain(currentNode,
token.getStartMark().getIndex(), token.getEndMark().getIndex(),
scalarToken.getValue() );
break;
}
addChildTerm(node);
checkForProblem(token.getStartMark(), node);
continue;
}
default:
continue;
}
}
}
private void handleScannerProblem(final ProblemInfo p) {
if (p.contextMark == null) {
if (p.problemMark != null && p.problemText != null) {
switch ((p.problemText.length() == 1) ? p.problemText.charAt(0) : 0) {
case ':':
case ',':
case '%':
case '-':
case '.':
case '?':
case '|':
case '>':
addChildTerm(new Dummy(STATUS2_SYNTAX_TOKEN_UNEXPECTED, this.currentNode,
p.problemMark.getIndex(), p.problemMark.getIndex() + p.problemText.length() ));
break;
default:
addChildTerm(new Dummy(STATUS2_SYNTAX_TOKEN_UNKNOWN, this.currentNode,
p.problemMark.getIndex(), p.problemMark.getIndex() + p.problemText.length() ));
}
}
}
else {
keepProblem(p);
}
}
private void keepProblem(final ProblemInfo p) {
final int contextIndex= p.contextMark.getIndex();
int idx= this.problemInfos.size();
while (idx > 0) {
if (this.problemInfos.get(idx - 1).contextMark.getIndex() <= contextIndex) {
break;
}
idx--;
}
this.problemInfos.add(idx, p);
}
private void clearProblems(final int contextIndex) {
int idx= 0;
while (idx < this.problemInfos.size()) {
if (this.problemInfos.get(idx).contextMark.getIndex() < contextIndex) {
idx++;
}
else {
break;
}
}
if (idx > 1) {
if (idx == 2) {
this.problemInfos.remove(0);
}
else {
this.problemInfos.subList(0, idx - 1).clear();
}
}
}
private void checkForProblem(final Mark startMark, final YamlAstNode node) {
final int contextIndex= startMark.getIndex();
for (int idx= 0; idx < this.problemInfos.size(); ) {
final ProblemInfo p= this.problemInfos.get(idx);
if (p.contextMark == startMark) {
if (attachProblem(node, p)) {
this.problemInfos.remove(idx);
continue;
}
}
if (p.contextMark.getIndex() > contextIndex) {
break;
}
idx++;
}
}
private boolean attachProblem(final YamlAstNode node, final ProblemInfo p) {
if (node.getStatusCode() != 0) {
return false;
}
switch (p.problem) {
case ScannerConstants.UNEXPECTED_CHAR:
case ScannerConstants.UNEXPECTED_CHAR_2:
if (p.problemMark != null && p.problemMark.getIndex() >= node.getStartOffset()
&& p.problemMark.getIndex() < node.getEndOffset()
&& p.problemText != null) {
node.status= STATUS2_SYNTAX_CHAR_INVALID;
node.addAttachment(new StatusDetail(
p.problemMark.getIndex(), p.problemText.length(),
p.problemText ));
return true;
}
break;
case ScannerConstants.UNEXPECTED_ESCAPE_SEQUENCE:
if (node.getNodeType() == NodeType.SCALAR) {
node.status= STATUS2_SYNTAX_ESCAPE_INVALID;
if (p.problemMark != null && p.problemText != null) {
node.addAttachment(new StatusDetail(
p.problemMark.getIndex() - 1, p.problemText.length() + 1,
'\\' + p.problemText ));
}
return true;
}
break;
case ScannerConstants.NOT_CLOSED:
if (node.getNodeType() == NodeType.SCALAR) {
node.status= STATUS2_SYNTAX_TOKEN_NOT_CLOSED;
return true;
}
break;
case ScannerConstants.MISSING_DIRECTIVE_NAME:
case ScannerConstants.UNEXPECTED_CHAR_FOR_VERSION_NUMBER:
case ScannerConstants.MISSING_URI:
case ScannerConstants.MISSING_ANCHOR_NAME:
case ScannerConstants.MISSING_MAP_COLON:
case ScannerConstants.UNEXPECTED_BLOCK_SEQ_ENTRY:
if (node.getClass() == Collection.BlockSeq.class) {
node.status= STATUS2_SYNTAX_TOKEN_UNEXPECTED | STATUS3_SEQ_ENTRY;
return true;
}
break;
case ScannerConstants.UNEXPECTED_MAP_KEY:
if (node.getNodeType() == NodeType.MAP_ENTRY) {
node.status= STATUS2_SYNTAX_TOKEN_UNEXPECTED | STATUS3_MAP_KEY;
return true;
}
if (node.getYamlParent() != null
&& node.getYamlParent().getNodeType() == NodeType.MAP_ENTRY
&& node.getYamlParent().getStatusCode() == 0) {
node.getYamlParent().status= STATUS2_SYNTAX_TOKEN_UNEXPECTED | STATUS3_MAP_KEY;
return true;
}
break;
case ScannerConstants.UNEXPECTED_MAP_VALUE:
if (node.getNodeType() == NodeType.MAP_ENTRY) {
node.status= STATUS2_SYNTAX_TOKEN_UNEXPECTED | STATUS3_MAP_VALUE;
return true;
}
if (node.getYamlParent() != null
&& node.getYamlParent().getNodeType() == NodeType.MAP_ENTRY
&& node.getYamlParent().getStatusCode() == 0) {
node.getYamlParent().status= STATUS2_SYNTAX_TOKEN_UNEXPECTED | STATUS3_MAP_VALUE;
return true;
}
break;
default:
break;
}
return false;
}
}