UNPKG

@creditkarma/thrift-parser

Version:

A parser for Thrift written in TypeScript

772 lines 31.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const factory_1 = require("./factory"); const debugger_1 = require("./debugger"); function isStatementBeginning(token) { switch (token.type) { case "NamespaceKeyword" /* NamespaceKeyword */: case "IncludeKeyword" /* IncludeKeyword */: case "ConstKeyword" /* ConstKeyword */: case "StructKeyword" /* StructKeyword */: case "UnionKeyword" /* UnionKeyword */: case "ExceptionKeyword" /* ExceptionKeyword */: case "ServiceKeyword" /* ServiceKeyword */: case "TypedefKeyword" /* TypedefKeyword */: case "EnumKeyword" /* EnumKeyword */: return true; default: return false; } } class ParseError extends Error { constructor(msg, loc) { super(msg); this.message = msg; this.loc = loc; } } function createParser(tokens, report = debugger_1.noopReporter) { let comments = []; let currentIndex = 0; // PUBLIC function parse() { const thrift = { type: "ThriftDocument" /* ThriftDocument */, body: [], }; while (!isAtEnd()) { try { const statement = parseStatement(); if (statement !== null) { thrift.body.push(statement); } } catch (e) { report(factory_1.createParseError(e.message, e.loc)); } } return thrift; } // Finds the beginning of the next statement so we can continue parse after error. function synchronize() { while (!isAtEnd() && !isStatementBeginning(currentToken())) { advance(); } } function parseStatement() { const next = currentToken(); // All Thrift statements must start with one of these types switch (next.type) { case "NamespaceKeyword" /* NamespaceKeyword */: return parseNamespace(); case "IncludeKeyword" /* IncludeKeyword */: return parseInclude(); case "ConstKeyword" /* ConstKeyword */: return parseConst(); case "StructKeyword" /* StructKeyword */: return parseStruct(); case "UnionKeyword" /* UnionKeyword */: return parseUnion(); case "ExceptionKeyword" /* ExceptionKeyword */: return parseException(); case "ServiceKeyword" /* ServiceKeyword */: return parseService(); case "TypedefKeyword" /* TypedefKeyword */: return parseTypedef(); case "EnumKeyword" /* EnumKeyword */: return parseEnum(); case "CommentBlock" /* CommentBlock */: case "CommentLine" /* CommentLine */: consumeComments(); return null; default: reportError(`Invalid start to Thrift statement ${next.text}`); } } // IncludeDefinition → 'include' StringLiteral function parseInclude() { const keywordToken = consume("IncludeKeyword" /* IncludeKeyword */); const pathToken = consume("StringLiteral" /* StringLiteral */); requireValue(pathToken, `Include statement must include a path as string literal`); return { type: "IncludeDefinition" /* IncludeDefinition */, path: factory_1.createStringLiteral(pathToken.text, pathToken.loc), comments: getComments(), loc: factory_1.createTextLocation(keywordToken.loc.start, pathToken.loc.end), }; } // ServiceDefinition → 'service' Identifier ( 'extends' Identifier )? '{' Function* '}' function parseService() { const keywordToken = consume("ServiceKeyword" /* ServiceKeyword */); const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `Unable to find identifier for service`); const extendsId = parseExtends(); const openBrace = consume("LeftBraceToken" /* LeftBraceToken */); requireValue(openBrace, `Expected opening curly brace`); const leadingComments = getComments(); const functions = parseFunctions(); const closeBrace = consume("RightBraceToken" /* RightBraceToken */); requireValue(closeBrace, `Expected closing curly brace`); const location = factory_1.createTextLocation(keywordToken.loc.start, closeBrace.loc.end); return { type: "ServiceDefinition" /* ServiceDefinition */, name: factory_1.createIdentifier(nameToken.text, nameToken.loc), extends: extendsId, functions, comments: [ ...leadingComments, ...getComments(), ], loc: location, }; } function parseExtends() { if (checkText('extends')) { const keywordToken = consume("ExtendsKeyword" /* ExtendsKeyword */); const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `Identifier expected after 'extends' keyword`); return factory_1.createIdentifier(nameToken.text, factory_1.createTextLocation(keywordToken.loc.start, nameToken.loc.end)); } else { return null; } } function parseFunctions() { const functions = []; while (!check("RightBraceToken" /* RightBraceToken */)) { if (check("CommentBlock" /* CommentBlock */, "CommentLine" /* CommentLine */)) { advance(); } else { functions.push(parseFunction()); if (isStatementBeginning(currentToken())) { reportError(`Closing curly brace expected, but new statement found`); } else if (check("EOF" /* EOF */)) { reportError(`Closing curly brace expected but reached end of file`); } } } return functions; } // Function → 'oneway'? FunctionType Identifier '(' Field* ')' Throws? ListSeparator? function parseFunction() { const onewayToken = consume("OnewayKeyword" /* OnewayKeyword */); const returnType = parseFunctionType(); const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `Unable to find function identifier`); const params = parseParameterFields(); requireValue(params, `List of zero or more fields expected`); const throws = parseThrows(); const listSeparator = readListSeparator(); const endLoc = ((listSeparator !== null) ? listSeparator.loc : (throws !== null) ? throws.loc : params.loc); return { type: "FunctionDefinition" /* FunctionDefinition */, name: factory_1.createIdentifier(nameToken.text, nameToken.loc), returnType, fields: params.fields, throws: (throws !== null) ? throws.fields : [], comments: getComments(), oneway: (onewayToken !== null), modifiers: ((onewayToken !== null) ? [onewayToken] : []), loc: { start: returnType.loc.start, end: endLoc.end, }, }; } function parseParameterFields() { const fields = []; const openParen = consume("LeftParenToken" /* LeftParenToken */); requireValue(openParen, `Opening paren expected to start list of fields`); while (!check("RightParenToken" /* RightParenToken */)) { readListSeparator(); fields.push(parseField()); if (isStatementBeginning(currentToken())) { reportError(`Closing paren ')' expected, but new statement found`); } else if (check("EOF" /* EOF */)) { reportError(`Closing paren ')' expected but reached end of file`); } } const closeParen = consume("RightParenToken" /* RightParenToken */); requireValue(closeParen, `Closing paren expected to end list of fields`); return { type: "ParametersDefinition" /* ParametersDefinition */, fields, loc: { start: openParen.loc.start, end: closeParen.loc.end, }, }; } // Throws → 'throws' '(' Field* ')' function parseThrows() { if (check("ThrowsKeyword" /* ThrowsKeyword */)) { const keywordToken = consume("ThrowsKeyword" /* ThrowsKeyword */); const params = parseParameterFields(); return { type: "ThrowsDefinition" /* ThrowsDefinition */, fields: params.fields, loc: { start: keywordToken.loc.start, end: params.loc.end, }, }; } return null; } // Namespace → 'namespace' ( NamespaceScope Identifier ) function parseNamespace() { const keywordToken = consume("NamespaceKeyword" /* NamespaceKeyword */); const scopeToken = consume("Identifier" /* Identifier */); requireValue(scopeToken, `Unable to find scope identifier for namespace`); const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `Unable to find name identifier for namespace`); return { type: "NamespaceDefinition" /* NamespaceDefinition */, scope: factory_1.createIdentifier(scopeToken.text, scopeToken.loc), name: factory_1.createIdentifier(nameToken.text, nameToken.loc), comments: getComments(), loc: factory_1.createTextLocation(keywordToken.loc.start, nameToken.loc.end), }; } // ConstDefinition → 'const' FieldType Identifier '=' ConstValue ListSeparator? function parseConst() { const keywordToken = consume("ConstKeyword" /* ConstKeyword */); const fieldType = parseFieldType(); const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `Const definition must have a name`); const initializer = parseValueAssignment(); requireValue(initializer, `Const must be initialized to a value`); readListSeparator(); return { type: "ConstDefinition" /* ConstDefinition */, name: factory_1.createIdentifier(nameToken.text, nameToken.loc), fieldType, initializer, comments: getComments(), loc: { start: keywordToken.loc.start, end: initializer.loc.end, }, }; } function parseValueAssignment() { if (check("EqualToken" /* EqualToken */)) { advance(); return parseValue(); } return null; } // TypedefDefinition → 'typedef' FieldType Identifier function parseTypedef() { const keywordToken = consume("TypedefKeyword" /* TypedefKeyword */); const type = parseFieldType(); const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `Typedef is expected to have name and none found`); return { type: "TypedefDefinition" /* TypedefDefinition */, name: factory_1.createIdentifier(nameToken.text, nameToken.loc), definitionType: type, comments: getComments(), loc: { start: keywordToken.loc.start, end: nameToken.loc.end, }, }; } // EnumDefinition → 'enum' Identifier '{' EnumMember* '}' function parseEnum() { const keywordToken = consume("EnumKeyword" /* EnumKeyword */); const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `Expected identifier for enum definition`); const openBrace = consume("LeftBraceToken" /* LeftBraceToken */); requireValue(openBrace, `Expected opening brace`); const members = parseEnumMembers(); const closeBrace = consume("RightBraceToken" /* RightBraceToken */); requireValue(closeBrace, `Expected closing brace`); const loc = { start: keywordToken.loc.start, end: closeBrace.loc.end, }; return { type: "EnumDefinition" /* EnumDefinition */, name: factory_1.createIdentifier(nameToken.text, nameToken.loc), members, comments: getComments(), loc, }; } function parseEnumMembers() { const members = []; while (!check("RightBraceToken" /* RightBraceToken */)) { if (check("CommentBlock" /* CommentBlock */, "CommentLine" /* CommentLine */)) { advance(); } else { members.push(parseEnumMember()); // consume list separator if there is one readListSeparator(); if (isStatementBeginning(currentToken())) { reportError(`Closing curly brace expected, but new statement found`); } else if (check("EOF" /* EOF */)) { reportError(`Closing curly brace expected but reached end of file`); } } } return members; } // EnumMember → (Identifier ('=' IntConstant)? ListSeparator?)* function parseEnumMember() { const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `EnumMember must have identifier`); let loc = null; let initializer = null; if (consume("EqualToken" /* EqualToken */) !== null) { const numToken = consume("IntegerLiteral" /* IntegerLiteral */, "HexLiteral" /* HexLiteral */); requireValue(numToken, `Equals token "=" must be followed by an Integer`); initializer = parseIntValue(numToken); loc = factory_1.createTextLocation(nameToken.loc.start, initializer.loc.end); } else { loc = factory_1.createTextLocation(nameToken.loc.start, nameToken.loc.end); } return { type: "EnumMember" /* EnumMember */, name: factory_1.createIdentifier(nameToken.text, nameToken.loc), initializer, comments: getComments(), loc, }; } // StructLike → ('struct' | 'union' | 'exception') Identifier 'xsd_all'? '{' Field* '}' function parseStructLikeInterface() { const keywordToken = consume("StructKeyword" /* StructKeyword */, "UnionKeyword" /* UnionKeyword */, "ExceptionKeyword" /* ExceptionKeyword */); const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `Struct-like must have an identifier`); const openBrace = consume("LeftBraceToken" /* LeftBraceToken */); requireValue(openBrace, `Struct-like body must begin with opening curly brace '{'`); const leadingComments = getComments(); const fields = parseFields(); const closeBrace = consume("RightBraceToken" /* RightBraceToken */); requireValue(closeBrace, `Struct-like body must end with a closing curly brace '}'`); return { name: factory_1.createIdentifier(nameToken.text, nameToken.loc), fields, comments: [ ...leadingComments, ...getComments(), ], loc: { start: keywordToken.loc.start, end: closeBrace.loc.end, }, }; } // StructDefinition → 'struct' Identifier 'xsd_all'? '{' Field* '}' function parseStruct() { const parsedData = parseStructLikeInterface(); return { type: "StructDefinition" /* StructDefinition */, name: parsedData.name, fields: parsedData.fields, comments: parsedData.comments, loc: parsedData.loc, }; } // UnioinDefinition → 'union' Identifier 'xsd_all'? '{' Field* '}' function parseUnion() { const parsedData = parseStructLikeInterface(); return { type: "UnionDefinition" /* UnionDefinition */, name: parsedData.name, fields: parsedData.fields.map((next) => { // As per the Thrift spec, all union fields are optional next.requiredness = 'optional'; return next; }), comments: parsedData.comments, loc: parsedData.loc, }; } // ExceptionDefinition → 'exception' Identifier '{' Field* '}' function parseException() { const parsedData = parseStructLikeInterface(); return { type: "ExceptionDefinition" /* ExceptionDefinition */, name: parsedData.name, fields: parsedData.fields, comments: parsedData.comments, loc: parsedData.loc, }; } function parseFields() { const fields = []; while (!check("RightBraceToken" /* RightBraceToken */)) { if (check("CommentBlock" /* CommentBlock */, "CommentLine" /* CommentLine */)) { advance(); } else { fields.push(parseField()); if (isStatementBeginning(currentToken())) { reportError(`Closing curly brace expected, but new statement found`); } else if (check("EOF" /* EOF */)) { reportError(`Closing curly brace expected but reached end of file`); } } } return fields; } // Field → FieldID? FieldReq? FieldType Identifier ('= ConstValue)? XsdFieldOptions ListSeparator? function parseField() { const startLoc = currentToken().loc; const fieldID = parseFieldId(); const fieldRequired = parserequireValuedness(); const fieldType = parseFieldType(); const nameToken = consume("Identifier" /* Identifier */); requireValue(nameToken, `Unable to find identifier for field`); const defaultValue = parseValueAssignment(); const listSeparator = readListSeparator(); const endLoc = ((listSeparator !== null) ? listSeparator.loc : (defaultValue !== null) ? defaultValue.loc : nameToken.loc); const location = factory_1.createTextLocation(startLoc.start, endLoc.end); return { type: "FieldDefinition" /* FieldDefinition */, name: factory_1.createIdentifier(nameToken.text, nameToken.loc), fieldID, fieldType, requiredness: fieldRequired, defaultValue, comments: getComments(), loc: location, }; } // ListSeparator → ',' | ';' function readListSeparator() { if (check("CommaToken" /* CommaToken */, "SemicolonToken" /* SemicolonToken */)) { return advance(); } return null; } // FieldRequired → 'required' | 'optional' function parserequireValuedness() { const current = currentToken(); if (current.text === 'required' || current.text === 'optional') { advance(); return current.text; } return null; } // FieldID → IntConstant ':' function parseFieldId() { if (currentToken().type === "IntegerLiteral" /* IntegerLiteral */ && peek().type === "ColonToken" /* ColonToken */) { const fieldIDToken = consume("IntegerLiteral" /* IntegerLiteral */); const colonToken = consume("ColonToken" /* ColonToken */); // return value of number token return factory_1.createFieldID(parseInt(fieldIDToken.text), factory_1.createTextLocation(fieldIDToken.loc.start, colonToken.loc.end)); } else { return null; } } // ConstValue → Literal | ConstMap | ConstList function parseValue() { const next = advance(); switch (next.type) { case "Identifier" /* Identifier */: return factory_1.createIdentifier(next.text, next.loc); case "StringLiteral" /* StringLiteral */: return factory_1.createStringLiteral(next.text, next.loc); case "IntegerLiteral" /* IntegerLiteral */: case "HexLiteral" /* HexLiteral */: return parseIntValue(next); case "FloatLiteral" /* FloatLiteral */: case "ExponentialLiteral" /* ExponentialLiteral */: return parseDoubleValue(next); case "TrueKeyword" /* TrueKeyword */: return factory_1.createBooleanLiteral(true, next.loc); case "FalseKeyword" /* FalseKeyword */: return factory_1.createBooleanLiteral(false, next.loc); case "LeftBraceToken" /* LeftBraceToken */: return parseMapValue(); case "LeftBracketToken" /* LeftBracketToken */: return parseListValue(); default: return null; } } function parseIntValue(token) { switch (token.type) { case "IntegerLiteral" /* IntegerLiteral */: return factory_1.createIntConstant(factory_1.createIntegerLiteral(token.text, token.loc), token.loc); case "HexLiteral" /* HexLiteral */: return factory_1.createIntConstant(factory_1.createHexLiteral(token.text, token.loc), token.loc); default: reportError(`IntConstant expected but found: ${token.type}`); } } function parseDoubleValue(token) { switch (token.type) { case "FloatLiteral" /* FloatLiteral */: return factory_1.createDoubleConstant(factory_1.createFloatLiteral(token.text, token.loc), token.loc); case "ExponentialLiteral" /* ExponentialLiteral */: return factory_1.createDoubleConstant(factory_1.createExponentialLiteral(token.text, token.loc), token.loc); default: reportError(`DoubleConstant expected but found: ${token.type}`); } } // ConstMap → '{' (ConstValue ':' ConstValue ListSeparator?)* '}' function parseMapValue() { // The parseValue method has already advanced the cursor const startLoc = currentToken().loc; const properties = check("RightBraceToken" /* RightBraceToken */) ? [] : readMapValues(); const closeBrace = consume("RightBraceToken" /* RightBraceToken */); requireValue(closeBrace, `Closing brace missing from map definition`); const endLoc = closeBrace.loc; const location = { start: startLoc.start, end: endLoc.end, }; return factory_1.createConstMap(properties, location); } // ConstList → '[' (ConstValue ListSeparator?)* ']' function parseListValue() { // The parseValue method has already advanced the cursor const startLoc = currentToken().loc; const elements = check("RightBracketToken" /* RightBracketToken */) ? [] : readListValues(); const closeBrace = consume("RightBracketToken" /* RightBracketToken */); requireValue(closeBrace, `Closing square-bracket missing from list definition`); const endLoc = closeBrace.loc; return factory_1.createConstList(elements, { start: startLoc.start, end: endLoc.end, }); } function readMapValues() { const properties = []; while (true) { const key = parseValue(); const semicolon = consume("ColonToken" /* ColonToken */); requireValue(semicolon, `Semicolon expected after key in map property assignment`); const value = parseValue(); properties.push(factory_1.creataePropertyAssignment(key, value, { start: key.loc.start, end: value.loc.end, })); if (check("CommaToken" /* CommaToken */)) { advance(); } else { break; } } return properties; } function readListValues() { const elements = []; while (true) { elements.push(parseValue()); if (check("CommaToken" /* CommaToken */, "SemicolonToken" /* SemicolonToken */)) { advance(); } else { break; } } return elements; } // FunctionType → FieldType | 'void' function parseFunctionType() { const typeToken = consume("VoidKeyword" /* VoidKeyword */); if (typeToken !== null) { return { type: "VoidKeyword" /* VoidKeyword */, loc: typeToken.loc, }; } else { return parseFieldType(); } } // FieldType → Identifier | BaseType | ContainerType function parseFieldType() { const typeToken = advance(); switch (typeToken.type) { case "Identifier" /* Identifier */: return factory_1.createIdentifier(typeToken.text, typeToken.loc); case "MapKeyword" /* MapKeyword */: return parseMapType(); case "ListKeyword" /* ListKeyword */: return parseListType(); case "SetKeyword" /* SetKeyword */: return parseSetType(); case "BinaryKeyword" /* BinaryKeyword */: case "BoolKeyword" /* BoolKeyword */: case "ByteKeyword" /* ByteKeyword */: case "StringKeyword" /* StringKeyword */: case "I8Keyword" /* I8Keyword */: case "I16Keyword" /* I16Keyword */: case "I32Keyword" /* I32Keyword */: case "I64Keyword" /* I64Keyword */: case "DoubleKeyword" /* DoubleKeyword */: return factory_1.createKeywordFieldType(typeToken.type, typeToken.loc); default: reportError(`FieldType expected but found: ${typeToken.type}`); } } // MapType → 'map' CppType? '<' FieldType ',' FieldType '>' function parseMapType() { const openBracket = consume("LessThanToken" /* LessThanToken */); requireValue(openBracket, `Map needs to defined contained types`); const keyType = parseFieldType(); const commaToken = consume("CommaToken" /* CommaToken */); requireValue(commaToken, `Comma expected to separate map types <key, value>`); const valueType = parseFieldType(); const closeBracket = consume("GreaterThanToken" /* GreaterThanToken */); requireValue(closeBracket, `Map needs to defined contained types`); const location = { start: openBracket.loc.start, end: closeBracket.loc.end, }; return factory_1.createMapFieldType(keyType, valueType, location); } // SetType → 'set' CppType? '<' FieldType '>' function parseSetType() { const openBracket = consume("LessThanToken" /* LessThanToken */); requireValue(openBracket, `Map needs to defined contained types`); const valueType = parseFieldType(); const closeBracket = consume("GreaterThanToken" /* GreaterThanToken */); requireValue(closeBracket, `Map needs to defined contained types`); return { type: "SetType" /* SetType */, valueType, loc: { start: openBracket.loc.start, end: closeBracket.loc.end, }, }; } // ListType → 'list' '<' FieldType '>' CppType? function parseListType() { const openBracket = consume("LessThanToken" /* LessThanToken */); requireValue(openBracket, `Map needs to defined contained types`); const valueType = parseFieldType(); const closeBracket = consume("GreaterThanToken" /* GreaterThanToken */); requireValue(closeBracket, `Map needs to defined contained types`); return { type: "ListType" /* ListType */, valueType, loc: { start: openBracket.loc.start, end: closeBracket.loc.end, }, }; } function consumeComments() { while (true) { const next = tokens[currentIndex]; switch (next.type) { case "CommentBlock" /* CommentBlock */: comments.push({ type: next.type, value: next.text.split('\n'), loc: next.loc, }); currentIndex++; break; case "CommentLine" /* CommentLine */: comments.push({ type: next.type, value: next.text, loc: next.loc, }); currentIndex++; break; default: return; } } } function currentToken() { consumeComments(); return tokens[currentIndex]; } function previousToken() { return tokens[currentIndex - 1]; } function peek() { return tokens[currentIndex + 1]; } // Does the current token match the given type function check(...types) { for (const type of types) { if (type === currentToken().type) { return true; } } return false; } // Does the current token match the given text function checkText(...strs) { for (const str of strs) { if (str === currentToken().text) { return true; } } return false; } // requireToken the current token to match given type and advance, otherwise return null function consume(...types) { for (const type of types) { if (check(type)) { return advance(); } } return null; } // Move the cursor forward and return the previous token function advance() { if (!isAtEnd()) { currentIndex += 1; } return previousToken(); } function isAtEnd() { return (currentIndex >= tokens.length || currentToken().type === "EOF" /* EOF */); } function getComments() { const current = comments; comments = []; return current; } function reportError(msg) { throw new ParseError(msg, previousToken().loc); } // Throw if the given value doesn't exist. function requireValue(val, msg) { if (val === null || val === undefined) { reportError(msg); } return val; } return { parse, synchronize, }; } exports.createParser = createParser; //# sourceMappingURL=parser.js.map