diff --git a/apidom/packages/apidom-ast/src/index.ts b/apidom/packages/apidom-ast/src/index.ts index d5773e5918..54f1d7cd19 100644 --- a/apidom/packages/apidom-ast/src/index.ts +++ b/apidom/packages/apidom-ast/src/index.ts @@ -39,6 +39,7 @@ export { default as YamlScalar } from './nodes/yaml/YamlScalar'; export { default as YamlSequence } from './nodes/yaml/YamlSequence'; export { default as YamlStream } from './nodes/yaml/YamlStream'; export { default as YamlTag } from './nodes/yaml/YamlTag'; +export { default as YamlAnchor } from './nodes/yaml/YamlAnchor'; export { isAlias as isYamlAlias, isKeyValuePair as isYamlKeyValuePair, @@ -57,8 +58,13 @@ export { default as Error } from './Error'; export { default as ParseResult } from './ParseResult'; // AST traversal related exports export { getVisitFn, BREAK, visit } from './visitor'; -// CST/AST transformers related exports +// JSON CST/AST transformers related exports export { transform as transformTreeSitterJsonCST, keyMap as treeSitterJsonKeyMap, } from './transformers/tree-sitter-json'; +// YAML CST/AST transformers related exports +export { + transform as transformTreeSitterYamlCST, + keyMap as treeSitterYamlKeyMap, +} from './transformers/tree-sitter-yaml'; diff --git a/apidom/packages/apidom-ast/src/nodes/yaml/YamlAnchor.ts b/apidom/packages/apidom-ast/src/nodes/yaml/YamlAnchor.ts new file mode 100644 index 0000000000..bb529687c8 --- /dev/null +++ b/apidom/packages/apidom-ast/src/nodes/yaml/YamlAnchor.ts @@ -0,0 +1,22 @@ +import stampit from 'stampit'; + +import Node from '../../Node'; + +interface YamlAnchor extends Node { + type: 'anchor'; + name: string | null; +} + +const YamlAnchor: stampit.Stamp = stampit(Node, { + statics: { + type: 'anchor', + }, + props: { + name: null, + }, + init({ name = null } = {}) { + this.name = name; + }, +}); + +export default YamlAnchor; diff --git a/apidom/packages/apidom-ast/src/nodes/yaml/YamlCollection.ts b/apidom/packages/apidom-ast/src/nodes/yaml/YamlCollection.ts index a00233cf5c..755dd28f09 100644 --- a/apidom/packages/apidom-ast/src/nodes/yaml/YamlCollection.ts +++ b/apidom/packages/apidom-ast/src/nodes/yaml/YamlCollection.ts @@ -2,9 +2,7 @@ import stampit from 'stampit'; import YamlNode from './YamlNode'; -interface YamlCollection extends YamlNode { - readonly children: Array; -} +type YamlCollection = YamlNode; const YamlCollection: stampit.Stamp = stampit(YamlNode, {}); diff --git a/apidom/packages/apidom-ast/src/nodes/yaml/YamlNode.ts b/apidom/packages/apidom-ast/src/nodes/yaml/YamlNode.ts index 7abb9b18ea..b01dda9fe4 100644 --- a/apidom/packages/apidom-ast/src/nodes/yaml/YamlNode.ts +++ b/apidom/packages/apidom-ast/src/nodes/yaml/YamlNode.ts @@ -1,12 +1,14 @@ import stampit from 'stampit'; import Node from '../../Node'; +import YamlTag from './YamlTag'; +import YamlAnchor from './YamlAnchor'; import { YamlStyle, YamlStyleGroup } from './YamlStyle'; interface YamlNode extends Node { content: unknown | null; - anchor: unknown | null; - tag: unknown | null; + anchor: YamlAnchor | null; + tag: YamlTag | null; style: YamlStyle; styleGroup: YamlStyleGroup; } diff --git a/apidom/packages/apidom-ast/src/nodes/yaml/YamlStream.ts b/apidom/packages/apidom-ast/src/nodes/yaml/YamlStream.ts index c63165ce07..f41607e08e 100644 --- a/apidom/packages/apidom-ast/src/nodes/yaml/YamlStream.ts +++ b/apidom/packages/apidom-ast/src/nodes/yaml/YamlStream.ts @@ -1,10 +1,13 @@ import stampit from 'stampit'; +import { isArray } from 'ramda-adjunct'; import Node from '../../Node'; import YamlDocument from './YamlDocument'; +import { isDocument } from './predicates'; interface YamlStream extends Node { type: 'stream'; + readonly content: Array; children: Array; } @@ -12,6 +15,11 @@ const YamlStream: stampit.Stamp = stampit(Node, { statics: { type: 'stream', }, + methods: { + get content(): Array { + return isArray(this.children) ? this.children.filter(isDocument) : []; + }, + }, }); export default YamlStream; diff --git a/apidom/packages/apidom-ast/src/nodes/yaml/YamlTag.ts b/apidom/packages/apidom-ast/src/nodes/yaml/YamlTag.ts index 3bee814679..e237e2e1ec 100644 --- a/apidom/packages/apidom-ast/src/nodes/yaml/YamlTag.ts +++ b/apidom/packages/apidom-ast/src/nodes/yaml/YamlTag.ts @@ -2,13 +2,13 @@ import stampit from 'stampit'; import Node from '../../Node'; -enum YamlNodeKind { +export enum YamlNodeKind { Scalar = 'Scalar', Sequence = 'Sequence', Mapping = 'Mapping', } -interface YamlTag { +interface YamlTag extends Node { type: 'tag'; name: string | null; kind: YamlNodeKind | null; diff --git a/apidom/packages/apidom-ast/src/transformers/tree-sitter-yaml.ts b/apidom/packages/apidom-ast/src/transformers/tree-sitter-yaml.ts new file mode 100644 index 0000000000..bdc700a284 --- /dev/null +++ b/apidom/packages/apidom-ast/src/transformers/tree-sitter-yaml.ts @@ -0,0 +1,248 @@ +import stampit from 'stampit'; +import { either, flatten, lensProp, over } from 'ramda'; +import { isArray, isFunction, isFalse } from 'ramda-adjunct'; +import { SyntaxNode, Tree } from 'tree-sitter'; + +import YamlStream from '../nodes/yaml/YamlStream'; +import YamlDocument from '../nodes/yaml/YamlDocument'; +import YamlSequence from '../nodes/yaml/YamlSequence'; +import YamlMapping from '../nodes/yaml/YamlMapping'; +import YamlKeyValuePair from '../nodes/yaml/YamlKeyValuePair'; +import YamlTag, { YamlNodeKind } from '../nodes/yaml/YamlTag'; +import YamlAnchor from '../nodes/yaml/YamlAnchor'; +import YamlScalar from '../nodes/yaml/YamlScalar'; +import { YamlStyle, YamlStyleGroup } from '../nodes/yaml/YamlStyle'; +import ParseResult from '../ParseResult'; +import Position, { Point } from '../Position'; +import Literal from '../Literal'; +import { isNode, visit } from '../visitor'; + +export const keyMap = { + stream: ['children'], + document: ['children'], + mapping: ['children'], + keyValuePair: ['children'], + sequence: ['children'], +}; + +const Visitor = stampit({ + init() { + /** + * Private API. + */ + + const toPosition = (node: SyntaxNode | null): Position | null => { + if (node === null) { + return null; + } + + const start = Point({ + row: node.startPosition.row, + column: node.startPosition.column, + char: node.startIndex, + }); + const end = Point({ + row: node.endPosition.row, + column: node.endPosition.column, + char: node.endIndex, + }); + + return Position({ start, end }); + }; + + const toTag = (node: SyntaxNode): YamlTag | null => { + let { previousSibling } = node; + + while (previousSibling !== null && previousSibling.type !== 'tag') { + ({ previousSibling } = previousSibling); + } + + if (previousSibling === null) { + return null; + } + + // eslint-disable-next-line no-nested-ternary + const kind = node.type.endsWith('mapping') + ? YamlNodeKind.Mapping + : node.type.endsWith('sequence') + ? YamlNodeKind.Sequence + : YamlNodeKind.Scalar; + const position = toPosition(previousSibling); + + return YamlTag({ name: previousSibling.text, kind, position }); + }; + + const toAnchor = (node: SyntaxNode): YamlAnchor | null => { + let { previousSibling } = node; + + while (previousSibling !== null && previousSibling.type !== 'anchor') { + ({ previousSibling } = previousSibling); + } + + if (previousSibling === null) { + return null; + } + + return YamlAnchor({ name: previousSibling.text, position: toPosition(previousSibling) }); + }; + + const flattenChildren = over(lensProp('children'), flatten); + + /** + * Public API. + */ + + this.enter = function enter(node: SyntaxNode) { + // missing anonymous literals from CST transformed into AST literal nodes + // WARNING: be aware that web-tree-sitter and tree-sitter node bindings have inconsistency + // in `SyntaxNode.isNamed` property. web-tree-sitter has it defined as method + // whether tree-sitter node binding has it defined as a boolean property. + // @ts-ignore + if ((isFunction(node.isNamed) && !node.isNamed()) || isFalse(node.isNamed)) { + const position = toPosition(node); + const value = node.type || node.text; + const isMissing = node.isMissing(); + + return Literal({ value, position, isMissing }); + } + + return undefined; + }; + + this.stream = { + enter(node: SyntaxNode) { + const position = toPosition(node); + + return YamlStream({ + children: node.children, + position, + isMissing: node.isMissing(), + }); + }, + }; + + this.document = { + enter(node: SyntaxNode) { + const position = toPosition(node); + + return YamlDocument({ + children: node.children, + position, + isMissing: node.isMissing(), + }); + }, + leave(node: YamlDocument) { + return flattenChildren(node); + }, + }; + + this.block_node = { + enter(node: SyntaxNode) { + return node.children; + }, + }; + + this.flow_node = { + enter(node: SyntaxNode) { + return node.children; + }, + }; + + this.tag = { + enter() { + return null; + }, + }; + + this.anchor = { + enter() { + return null; + }, + }; + + this.block_mapping = { + enter(node: SyntaxNode) { + const position = toPosition(node); + const tag = toTag(node); + const anchor = toAnchor(node); + + return YamlMapping({ + children: node.children, + position, + anchor, + tag, + styleGroup: YamlStyleGroup.Block, + style: YamlStyle.NextLine, + isMissing: node.isMissing(), + }); + }, + }; + + this.block_mapping_pair = { + enter(node: SyntaxNode) { + const position = toPosition(node); + + return YamlKeyValuePair({ + children: node.children, + position, + isMissing: node.isMissing(), + }); + }, + }; + + this.keyValuePair = { + leave(node: YamlKeyValuePair) { + return flattenChildren(node); + }, + }; + + this.flow_sequence = { + enter(node: SyntaxNode) { + const position = toPosition(node); + const tag = toTag(node); + const anchor = toAnchor(node); + + return YamlSequence({ + children: node.children, + position, + anchor, + tag, + styleGroup: YamlStyleGroup.Flow, + style: YamlStyle.Explicit, + }); + }, + }; + + this.sequence = { + leave(node: YamlSequence) { + return flattenChildren(node); + }, + }; + + this.plain_scalar = { + enter(node: SyntaxNode) { + const position = toPosition(node); + const tag = toTag(node); + const anchor = toAnchor(node); + + return YamlScalar({ + content: node.text, + anchor, + tag, + position, + styleGroup: YamlStyleGroup.Flow, + style: YamlStyle.Plain, + }); + }, + }; + }, +}); + +export const transform = (cst: Tree): ParseResult => { + const visitor = Visitor(); + const nodePredicate = either(isArray, isNode); + // @ts-ignore + const rootNode = visit(cst.rootNode, visitor, { keyMap, nodePredicate }); + + return ParseResult({ children: [rootNode] }); +}; diff --git a/apidom/packages/apidom-ast/src/visitor.ts b/apidom/packages/apidom-ast/src/visitor.ts index 67778e7812..c87981f2ef 100644 --- a/apidom/packages/apidom-ast/src/visitor.ts +++ b/apidom/packages/apidom-ast/src/visitor.ts @@ -37,10 +37,10 @@ export const getVisitFn = (visitor, type: string, isLeaving: boolean) => { export const BREAK = {}; // getNodeType :: Node -> String -const getNodeType = prop('type'); +export const getNodeType = prop('type'); // isNode :: Node -> Boolean -const isNode = curryN(1, pipe(getNodeType, isString)); +export const isNode = curryN(1, pipe(getNodeType, isString)); /* eslint-disable no-continue, no-nested-ternary, no-param-reassign */ /** diff --git a/apidom/packages/apidom-ast/test/transformers/tree-sitter-json.ts b/apidom/packages/apidom-ast/test/transformers/tree-sitter-json.ts index 7893da74ea..ad0f24494d 100644 --- a/apidom/packages/apidom-ast/test/transformers/tree-sitter-json.ts +++ b/apidom/packages/apidom-ast/test/transformers/tree-sitter-json.ts @@ -1,3 +1,4 @@ +// @ts-ignore import Parser from 'tree-sitter'; import { assert } from 'chai'; // @ts-ignore diff --git a/apidom/packages/apidom-ast/test/transformers/tree-sitter-yaml.ts b/apidom/packages/apidom-ast/test/transformers/tree-sitter-yaml.ts new file mode 100644 index 0000000000..470327ad24 --- /dev/null +++ b/apidom/packages/apidom-ast/test/transformers/tree-sitter-yaml.ts @@ -0,0 +1,29 @@ +// @ts-ignore +import Parser from 'tree-sitter'; +import { assert } from 'chai'; +// @ts-ignore +import YAMLLanguage from 'tree-sitter-yaml'; + +import { ParseResult, transformTreeSitterYamlCST as transform } from '../../src'; + +describe('tree-sitter-yaml', function () { + context('given error-less CST to AST transformation', function () { + let cst: Parser.Tree; + let ast: ParseResult; + + beforeEach(function () { + const parser = new Parser(); + parser.setLanguage(YAMLLanguage); + + const jsonString = '[1, null]'; + cst = parser.parse(jsonString); + ast = transform(cst); + }); + + context('ParseResult', function () { + specify('should be the result of transformation', function () { + assert.propertyVal(ast, 'type', 'parseResult'); + }); + }); + }); +});