@neo4j-cypher/language-support
Version:
Cypher language support
220 lines (183 loc) • 5.47 kB
text/typescript
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore There is a default export but not in the types
import antlrDefaultExport, {
CommonTokenStream,
ParserRuleContext,
ParseTree,
Token,
} from 'antlr4';
import CypherLexer from './generated-parser/CypherCmdLexer';
import CypherParser, {
NodePatternContext,
RelationshipPatternContext,
StatementsOrCommandsContext,
} from './generated-parser/CypherCmdParser';
import { ParsedStatement, ParsingResult } from './parserWrapper';
/* In antlr we have
ParseTree
/ \
/ \
TerminalNode RuleContext
\
ParserRuleContext
Both TerminalNode and RuleContext have parentCtx, but ParseTree doesn't
This type fixes that because it's what we need to traverse the tree most
of the time
*/
export type EnrichedParseTree = ParseTree & {
parentCtx: ParserRuleContext | undefined;
};
export function findStopNode(root: StatementsOrCommandsContext) {
let children = root.children;
let current: ParserRuleContext = root;
while (children && children.length > 0) {
let index = children.length - 1;
let child = children[index];
while (
index > 0 &&
(child === root.EOF() ||
child.getText() === '' ||
child.getText().startsWith('<missing'))
) {
index--;
child = children[index];
}
current = child as ParserRuleContext;
children = current.children;
}
return current;
}
export function findParent(
leaf: EnrichedParseTree | undefined,
condition: (node: EnrichedParseTree) => boolean,
): EnrichedParseTree {
let current: EnrichedParseTree | undefined = leaf;
while (current && !condition(current)) {
current = current.parentCtx;
}
return current;
}
export function getTokens(tokenStream: CommonTokenStream): Token[] {
// FIXME The type of .tokens is string[], it seems wrong in the antlr4 library
// Fix this after we've raised an issue and a PR and has been corrected in antlr4
return tokenStream.tokens as unknown as Token[];
}
export function isDefined(x: unknown) {
return x !== null && x !== undefined;
}
type AntlrDefaultExport = {
tree: {
Trees: {
getNodeText(
node: ParserRuleContext,
s: string[],
c: typeof CypherParser,
): string;
getChildren(node: ParserRuleContext): ParserRuleContext[];
};
};
};
export const antlrUtils = antlrDefaultExport as unknown as AntlrDefaultExport;
export function inNodeLabel(stopNode: ParserRuleContext) {
const nodePattern = findParent(
stopNode,
(p) => p instanceof NodePatternContext,
);
return isDefined(nodePattern);
}
export function inRelationshipType(stopNode: ParserRuleContext) {
const relPattern = findParent(
stopNode,
(p) => p instanceof RelationshipPatternContext,
);
return isDefined(relPattern);
}
export function findCaret(
parsingResult: ParsingResult,
caretPosition: number,
): { statement: ParsedStatement; token: Token } | undefined {
const statements = parsingResult.statementsParsing;
let i = 0;
let result: { statement: ParsedStatement; token: Token } = undefined;
let keepLooking = true;
while (i < statements.length && keepLooking) {
let j = 0;
const statement = statements[i];
const tokens = statement.tokens;
while (j < tokens.length && keepLooking) {
const currentToken = tokens[j];
keepLooking = currentToken.start <= caretPosition;
if (currentToken.channel === 0 && keepLooking) {
result = { statement: statement, token: currentToken };
}
j++;
}
i++;
}
return result;
}
export function splitIntoStatements(
tokenStream: CommonTokenStream,
lexer: CypherLexer,
): CommonTokenStream[] {
tokenStream.fill();
const tokens = tokenStream.tokens;
let i = 0;
const result: CommonTokenStream[] = [];
let chunk: Token[] = [];
let offset = 0;
while (i < tokens.length) {
const current = tokens[i].clone();
current.tokenIndex -= offset;
chunk.push(current);
if (
current.type === CypherLexer.SEMICOLON ||
current.type === CypherLexer.EOF
) {
// This does not relex since we are not calling fill on the token stream
const tokenStream = new CommonTokenStream(lexer);
tokenStream.tokens = chunk;
result.push(tokenStream);
offset = i + 1;
chunk = [];
}
i++;
}
return result;
}
export function findPreviousNonSpace(
tokens: Token[],
index: number,
): Token | undefined {
let i = index;
while (i > 0) {
const token = tokens[--i];
if (token.type !== CypherParser.SPACE) {
return token;
}
}
return undefined;
}
export function isCommentOpener(
thisToken: Token,
nextToken: Token | undefined,
): boolean {
return thisToken.text === '/' && nextToken?.text === '*';
}
export const rulesDefiningVariables = [
CypherParser.RULE_returnItem,
CypherParser.RULE_unwindClause,
CypherParser.RULE_subqueryInTransactionsReportParameters,
CypherParser.RULE_procedureResultItem,
CypherParser.RULE_foreachClause,
CypherParser.RULE_loadCSVClause,
CypherParser.RULE_reduceExpression,
CypherParser.RULE_listItemsPredicate,
CypherParser.RULE_listComprehension,
];
export const rulesDefiningOrUsingVariables = [
...rulesDefiningVariables,
CypherParser.RULE_pattern,
CypherParser.RULE_nodePattern,
CypherParser.RULE_relationshipPattern,
];