diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..27bb0f7a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +modules/treesitter/src/main/scala/playground/generated/** linguist-generated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e08593a8..b12178df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,3 +57,20 @@ jobs: - name: Show extension test logs if: always() && job.status == 'failure' run: cat vscode-extension/fixture/smithyql-log.txt | tail --lines 1000 + + build-parser: + name: "Build parser" + strategy: + matrix: + os: [ubuntu-latest, macos-latest, macos-13] + runs-on: ${{matrix.os}} + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4.1.1 + - uses: coursier/setup-action@v1 + with: + apps: sbt + jvm: adoptium:1.21 + - name: Parser tests + # intentionally not setting up nix + run: sbt treesitter/test diff --git a/.gitignore b/.gitignore index afc0453f..8c95b2bd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ smithyql-log.txt result .version +build/smithy diff --git a/build.sbt b/build.sbt index 65ef893f..c9783e18 100644 --- a/build.sbt +++ b/build.sbt @@ -87,6 +87,7 @@ val commonSettings = Seq( Test / scalacOptions += "-Wconf:cat=deprecation:silent,msg=Specify both message and version:silent", scalacOptions += "-release:11", mimaFailOnNoPrevious := false, + resolvers += "Sonatype S01 snapshots" at "https://s01.oss.sonatype.org/content/repositories/snapshots", ) def module( @@ -133,8 +134,29 @@ lazy val parser = module("parser") .dependsOn( ast % "test->test;compile->compile", source % "test->test;compile->compile", + treesitter % "test->compile", ) +lazy val treesitter = module("treesitter") + .settings( + libraryDependencies ++= Seq( + "org.polyvariant.treesitter4s" %% "core" % "0.4.0" + ) + ) + +lazy val parsergen = module("parser-gen") + .settings( + libraryDependencies ++= Seq( + "dev.optics" %% "monocle-core" % "3.3.0", + "com.disneystreaming.smithy4s" %% "smithy4s-json" % smithy4sVersion.value, + ("org.scalameta" %% "scalameta" % "4.11.0").cross(CrossVersion.for3Use2_13), + "org.polyvariant.treesitter4s" %% "core" % "0.4.0", + "com.lihaoyi" %% "os-lib" % "0.11.3", + ), + scalacOptions -= "-release:11", + ) + .enablePlugins(Smithy4sCodegenPlugin) + // Formatter for the SmithyQL language constructs lazy val formatter = module("formatter") .settings( @@ -189,6 +211,7 @@ lazy val core = module("core") examples % "test->compile", pluginCore, ast, + treesitter, source % "test->test;compile->compile", parser % "test->compile;test->test", formatter, @@ -239,6 +262,7 @@ lazy val e2e = module("e2e") parser / publishLocal, pluginCore / publishLocal, source / publishLocal, + treesitter / publishLocal, ast / publishLocal, formatter / publishLocal, protocol4s / publishLocal, @@ -269,6 +293,7 @@ lazy val root = project core, examples, parser, + parsergen, formatter, languageSupport, lsp, @@ -276,4 +301,5 @@ lazy val root = project pluginCore, pluginSample, e2e, + treesitter, ) diff --git a/flake.lock b/flake.lock index 0ef3c0b0..f448bae3 100644 --- a/flake.lock +++ b/flake.lock @@ -20,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1730602179, - "narHash": "sha256-efgLzQAWSzJuCLiCaQUCDu4NudNlHdg2NzGLX5GYaEY=", + "lastModified": 1734875076, + "narHash": "sha256-Pzyb+YNG5u3zP79zoi8HXYMs15Q5dfjDgwCdUI5B0nY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3c2f1c4ca372622cb2f9de8016c9a0b1cbd0f37c", + "rev": "1807c2b91223227ad5599d7067a61665c52d1295", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-24.05", + "ref": "nixos-24.11", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index c81ba451..06149495 100644 --- a/flake.nix +++ b/flake.nix @@ -1,16 +1,13 @@ { inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem ( - system: - let - pkgs = import nixpkgs { inherit system; }; - in - { + flake-utils.lib.eachDefaultSystem (system: + let pkgs = import nixpkgs { inherit system; }; + in { devShells.default = pkgs.mkShell { buildInputs = [ pkgs.yarn @@ -18,10 +15,33 @@ pkgs.sbt pkgs.jless pkgs.gnupg + (pkgs.tree-sitter.override { webUISupport = true; }) # temporary, while we don't download coursier ourselves pkgs.coursier ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.xvfb-run ]; }; - } - ); + packages.tree-sitter-smithyql = pkgs.stdenv.mkDerivation { + name = "tree-sitter-smithyql"; + src = ./tree-sitter-smithyql; + buildInputs = [ pkgs.tree-sitter pkgs.nodejs ]; + buildPhase = '' + tree-sitter generate + cc src/parser.c -shared -o $out + ''; + dontInstall = true; + }; + packages.tree-sitter-smithyql-all = pkgs.stdenv.mkDerivation { + name = "tree-sitter-smithyql-all"; + src = ./tree-sitter-smithyql; + dontBuild = true; + installPhase = '' + mkdir $out + cd $out + mkdir darwin-aarch64 && cp ${self.packages.aarch64-darwin.tree-sitter-smithyql} darwin-aarch64/libtree-sitter-smithyql.dylib + mkdir darwin-x86-64 && cp ${self.packages.x86_64-darwin.tree-sitter-smithyql} darwin-x86-64/libtree-sitter-smithyql.dylib + mkdir linux-aarch64 && cp ${self.packages.aarch64-linux.tree-sitter-smithyql} linux-aarch64/libtree-sitter-smithyql.so + mkdir linux-x86-64 && cp ${self.packages.x86_64-linux.tree-sitter-smithyql} linux-x86-64/libtree-sitter-smithyql.so + ''; + }; + }); } diff --git a/modules/core/src/main/scala/playground/ASTAdapter.scala b/modules/core/src/main/scala/playground/ASTAdapter.scala new file mode 100644 index 00000000..c7d6816f --- /dev/null +++ b/modules/core/src/main/scala/playground/ASTAdapter.scala @@ -0,0 +1,12 @@ +package playground + +import cats.syntax.all.* +import playground.smithyql.QualifiedIdentifier + +object ASTAdapter { + + def decodeQI(qi: playground.generated.nodes.QualifiedIdentifier): Option[QualifiedIdentifier] = + (qi.namespace.map(_.source).toNel, qi.selection.map(_.source)) + .mapN(QualifiedIdentifier.apply) + +} diff --git a/modules/core/src/main/scala/playground/MultiServiceResolver.scala b/modules/core/src/main/scala/playground/MultiServiceResolver.scala index 15c5fc7c..ff35f20f 100644 --- a/modules/core/src/main/scala/playground/MultiServiceResolver.scala +++ b/modules/core/src/main/scala/playground/MultiServiceResolver.scala @@ -9,6 +9,7 @@ import playground.smithyql.UseClause import playground.smithyql.WithSource object MultiServiceResolver { + import playground.smithyql.tsutils.* /** Determines which service should be used for a query. The rules are: * - If the operation name has a service identifier, there MUST be a service with that name @@ -37,6 +38,34 @@ object MultiServiceResolver { case None => resolveImplicit(queryOperationName.operationName, serviceIndex, useClauses) } + /** Determines which service should be used for a query. The rules are: + * - If the operation name has a service identifier, there MUST be a service with that name + * that contains the given operation. + * - If there's no service identifier, find all matching services that are included in the use + * clauses. MUST find exactly one entry. + * + * In other cases, such as when we can't find a unique entry, or the explicitly referenced + * service doesn't have an operation with a matching name, we fail. The latter might eventually + * be refactored to a separate piece of code. + * + * **Important**! + * + * This method assumes that all of the use clauses match the available service set. It does NOT + * perform a check on that. For the actual check, see PreludeCompiler. + */ + def resolveServiceTs( + queryOperationName: playground.generated.nodes.QueryOperationName, + serviceIndex: ServiceIndex, + useClauses: List[playground.generated.nodes.UseClause], + ): EitherNel[CompilationError, Option[QualifiedIdentifier]] = queryOperationName + .name + .flatTraverse { opName => + queryOperationName.service_identifier match { + case Some(explicitRef) => resolveExplicitTs(serviceIndex, explicitRef, opName) + case None => resolveImplicitTs(opName, serviceIndex, useClauses).map(_.some) + } + } + private def resolveExplicit( index: ServiceIndex, explicitRef: WithSource[QualifiedIdentifier], @@ -66,6 +95,39 @@ object MultiServiceResolver { case Some(_) => explicitRef.value.asRight } + private def resolveExplicitTs( + index: ServiceIndex, + explicitRef: playground.generated.nodes.QualifiedIdentifier, + operationName: playground.generated.nodes.OperationName, + ): EitherNel[CompilationError, Option[QualifiedIdentifier]] = ASTAdapter + .decodeQI(explicitRef) + .traverse { ref => + index.getService(ref) match { + // explicit reference exists, but the service doesn't + case None => + CompilationError + .error( + CompilationErrorDetails.UnknownService(index.serviceIds.toList), + explicitRef.range, + ) + .leftNel + + // the service exists, but doesn't have the requested operation + case Some(service) + if !service.operationNames.contains_(OperationName(operationName.source)) => + CompilationError + .error( + CompilationErrorDetails.OperationMissing(service.operationNames.toList), + operationName.range, + ) + .leftNel + + // all good + case Some(_) => ref.asRight + } + + } + private def resolveImplicit( operationName: WithSource[OperationName[WithSource]], index: ServiceIndex, @@ -90,4 +152,28 @@ object MultiServiceResolver { } } + private def resolveImplicitTs( + operationName: playground.generated.nodes.OperationName, + index: ServiceIndex, + useClauses: List[playground.generated.nodes.UseClause], + ): EitherNel[CompilationError, QualifiedIdentifier] = { + val matchingServices = index + .getServices(useClauses.flatMap(_.identifier).flatMap(ASTAdapter.decodeQI).toSet) + .filter(_.hasOperation(OperationName(operationName.source))) + + matchingServices match { + case one :: Nil => one.id.asRight + case _ => + CompilationError + .error( + CompilationErrorDetails + .AmbiguousService( + workspaceServices = index.serviceIds.toList + ), + operationName.range, + ) + .leftNel + } + } + } diff --git a/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala b/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala index e651290c..144fd96e 100644 --- a/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala +++ b/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala @@ -1,166 +1,96 @@ package playground.smithyql import cats.syntax.all.* +import tsutils.* +import util.chaining.* trait RangeIndex { def findAtPosition( pos: Position - ): NodeContext + ): Option[NodeContext] } object RangeIndex { - def build( - sf: SourceFile[WithSource] - ): RangeIndex = - new RangeIndex { - - private val allRanges: List[ContextRange] = { - val path = NodeContext.EmptyPath - - val preludeRanges: List[ContextRange] = sf - .prelude - .useClauses - .toNel - .foldMap { useClauses => - val newBase = path.inPrelude - - ContextRange(useClauses.map(_.range).reduceLeft(_.fakeUnion(_)), newBase) :: - sf.prelude - .useClauses - .mapWithIndex { - ( - uc, - i, - ) => - findInUseClause(uc, newBase.inUseClause(i)) - } - .combineAll - } + def build(parsed: playground.generated.nodes.SourceFile): RangeIndex = fromRanges { - val queryRanges = sf.queries(WithSource.unwrap).zipWithIndex.flatMap { case (rq, index) => - findInQuery(rq.query, path.inQuery(index)) - } + val root = NodeContext.EmptyPath - preludeRanges ++ queryRanges + val preludeRanges = parsed + .prelude + .toList + .flatMap { prelude => + val newBase = root.inPrelude + ContextRange(prelude.range, newBase) :: + prelude.use_clause.zipWithIndex.map { (useClause, i) => + ContextRange(useClause.range, newBase.inUseClause(i)) + } } - // Console - // .err - // .println( - // s"""Found ${allRanges.size} ranges for query ${q.operationName.value.text}: - // |${allRanges - // .map(_.render) - // .mkString("\n")}""".stripMargin - // ) - - def findAtPosition( - pos: Position - ): NodeContext = allRanges - .filter(_.range.contains(pos)) - .maxByOption(_.ctx.length) - .map(_.ctx) - // By default, we're on root level - .getOrElse(NodeContext.EmptyPath) - - } - - private def findInQuery( - q: WithSource[Query[WithSource]], - path: NodeContext, - ) = { - val qv = q.value - - List(ContextRange(q.range, path)) ++ - findInOperationName(qv.operationName, path.inOperationName) ++ - findInNode(qv.input, path.inOperationInput) - } - - private def findInUseClause( - useClause: WithSource[UseClause[WithSource]], - path: NodeContext, - ): List[ContextRange] = ContextRange(useClause.value.identifier.range, path) :: Nil - - private def findInOperationName( - operationName: WithSource[QueryOperationName[WithSource]], - path: NodeContext, - ): List[ContextRange] = - ContextRange( - operationName.value.operationName.range, - path, - ) :: Nil - - private def findInNode( - node: WithSource[InputNode[WithSource]], - ctx: NodeContext, - ): List[ContextRange] = { - def entireNode( - ctx: NodeContext - ) = ContextRange(node.range, ctx) - - val default = Function.const( - // Default case: can be triggered e.g. inside a string literal - // which would affect completions of enum values and timestamps. - entireNode(ctx) :: Nil + def inputNodeRanges(node: playground.generated.nodes.InputNode, base: NodeContext) + : List[ContextRange] = node.visit( + new playground.generated.nodes.InputNode.Visitor.Default[List[ContextRange]] { + def default: List[ContextRange] = Nil + + override def onString(node: playground.generated.nodes.String_): List[ContextRange] = + ContextRange(node.range.shrink1, base.inQuotes) :: Nil + + override def onList(node: playground.generated.nodes.List_): List[ContextRange] = + ContextRange(node.range.shrink1, base.inCollectionEntry(None)) :: + node.list_fields.zipWithIndex.flatMap { (inputNode, i) => + ContextRange(inputNode.range, base.inCollectionEntry(Some(i))) :: + inputNodeRanges(inputNode, base.inCollectionEntry(Some(i))) + } + + override def onStruct(node: playground.generated.nodes.Struct): List[ContextRange] = + ContextRange(node.range.shrink1, base.inStructBody) :: + node.bindings.toList.flatMap { binding => + (binding.key, binding.value).tupled.toList.flatMap { (key, value) => + ContextRange(value.range, base.inStructBody.inStructValue(key.source)) :: + inputNodeRanges(value, base.inStructBody.inStructValue(key.source)) + } + } + } ) - node - .value - .fold( - listed = l => entireNode(ctx) :: findInList(l, ctx), - struct = s => entireNode(ctx) :: findInStruct(s, ctx.inStructBody), - string = { _ => - val inQuotes = ContextRange( - node.range.shrink1, - ctx.inQuotes, - ) - - inQuotes :: entireNode(ctx) :: Nil - }, - int = default, - bool = default, - nul = default, - ) + val queryRanges = parsed.statements.zipWithIndex.flatMap { (stat, statementIndex) => + stat.run_query.toList.flatMap { runQuery => + ContextRange(runQuery.range, root.inQuery(statementIndex)) :: runQuery + .operation_name + .toList + .flatMap { operationName => + ContextRange(operationName.range, root.inQuery(statementIndex).inOperationName) :: Nil + } ++ + runQuery.input.toList.flatMap { input => + inputNodeRanges( + playground.generated.nodes.InputNode(input), + root.inQuery(statementIndex).inOperationInput, + ) - } + } + } - private def findInList( - list: Listed[WithSource], - ctx: NodeContext, - ): List[ContextRange] = { - val inItems = list - .values - .value - .zipWithIndex - .flatMap { case (entry, index) => findInNode(entry, ctx.inCollectionEntry(index.some)) } - - val inBody = ContextRange( - list - .values - .range, - ctx.inCollectionEntry(None), - ) + } - inBody :: inItems + preludeRanges ++ queryRanges } - private def findInStruct( - struct: Struct[WithSource], - ctx: NodeContext, - ): List[ContextRange] = { - // Struct fields that allow nesting in them - val inFields = struct - .fields - .value - .value - .flatMap { binding => - findInNode(binding.value, ctx.inStructValue(binding.identifier.value.text)) - } - - ContextRange(struct.fields.range, ctx) :: inFields - } + def fromRanges(allRanges: List[ContextRange]): RangeIndex = + pos => + allRanges + .filter(_.range.contains(pos)) + .tap { ranges => + // println() + // println("=======") + // println(s"all ranges: ${allRanges.map(_.render).mkString(", ")}") + // println(s"ranges for position ${pos.index}: ${ranges.map(_.render).mkString(", ")}") + // println("=======") + // println() + } + .maxByOption(_.ctx.length) + .map(_.ctx) } diff --git a/modules/core/src/main/scala/playground/smithyql/tsutils.scala b/modules/core/src/main/scala/playground/smithyql/tsutils.scala new file mode 100644 index 00000000..5cb55005 --- /dev/null +++ b/modules/core/src/main/scala/playground/smithyql/tsutils.scala @@ -0,0 +1,11 @@ +package playground.smithyql + +import org.polyvariant.treesitter4s.Node + +object tsutils { + + extension (node: Node) { + def range: SourceRange = SourceRange(Position(node.startByte), Position(node.endByte)) + } + +} diff --git a/modules/core/src/test/scala/playground/smithyql/AtPositionTests.scala b/modules/core/src/test/scala/playground/smithyql/AtPositionTests.scala index 1c335c7d..e83f7611 100644 --- a/modules/core/src/test/scala/playground/smithyql/AtPositionTests.scala +++ b/modules/core/src/test/scala/playground/smithyql/AtPositionTests.scala @@ -1,8 +1,9 @@ package playground.smithyql +import cats.syntax.all.* +import org.polyvariant.treesitter4s.TreeSitterAPI import playground.Assertions.* import playground.Diffs.given -import playground.smithyql.parser.SourceParser import weaver.* object AtPositionTests extends FunSuite { @@ -24,15 +25,16 @@ object AtPositionTests extends FunSuite { text: String ): NodeContext = { val (extracted, position) = extractCursor(text) - val parsed = - SourceParser[SourceFile] - .parse(extracted) - .toTry - .get + val parsedTs = playground + .generated + .nodes + .SourceFile + .unsafeApply(TreeSitterAPI.make("smithyql").parse(extracted).rootNode.get) RangeIndex - .build(parsed) + .build(parsedTs) .findAtPosition(position) + .getOrElse(NodeContext.EmptyPath) } // tests for before/after/between queries diff --git a/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala b/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala index 58c99f4e..ddf18c29 100644 --- a/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala +++ b/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala @@ -3,6 +3,8 @@ package playground.language import cats.Id import cats.kernel.Order.catsKernelOrderingForOrder import cats.syntax.all.* +import org.polyvariant.treesitter4s.TreeSitterAPI +import playground.ASTAdapter import playground.MultiServiceResolver import playground.ServiceIndex import playground.smithyql.NodeContext @@ -11,11 +13,7 @@ import playground.smithyql.NodeContext.^^: import playground.smithyql.OperationName import playground.smithyql.Position import playground.smithyql.QualifiedIdentifier -import playground.smithyql.Query import playground.smithyql.RangeIndex -import playground.smithyql.SourceFile -import playground.smithyql.WithSource -import playground.smithyql.parser.SourceParser import playground.smithyql.syntax.* import smithy4s.dynamic.DynamicSchemaIndex @@ -75,14 +73,15 @@ object CompletionProvider { } def completeRootOperationName( - file: SourceFile[WithSource], + file: playground.generated.nodes.SourceFile, insertBodyStruct: CompletionItem.InsertBodyStruct, ) = { // double-check test coverage. // there's definitely a test missing for N>1 clauses. // https://github.com/kubukoz/smithy-playground/issues/161 - val presentServiceIds - : List[QualifiedIdentifier] = file.prelude.useClauses.map(_.value.identifier.value) + val presentServiceIds: List[QualifiedIdentifier] = file + .select(_.prelude.use_clause.identifier) + .flatMap(ASTAdapter.decodeQI) // for operations on root level we show: // - completions for ops from the service being used, which don't insert a use clause and don't show the service ID @@ -112,17 +111,18 @@ object CompletionProvider { // we're definitely in an existing query, so we don't insert a brace in either case. def completeOperationNameFor( - q: Query[WithSource], - sf: SourceFile[WithSource], + q: playground.generated.nodes.RunQuery, + sf: playground.generated.nodes.SourceFile, serviceId: Option[QualifiedIdentifier], ): List[CompletionItem] = serviceId match { case Some(serviceId) => // includes the current query's service reference // as it wouldn't result in ading a use clause - val presentServiceIdentifiers = - q.operationName.value.mapK(WithSource.unwrap).identifier.toList ++ - sf.prelude.useClauses.map(_.value.identifier.value) + val presentServiceIdentifiers = { + q.select(_.operation_name.service_identifier) ++ + sf.select(_.prelude.use_clause.identifier) + }.flatMap(ASTAdapter.decodeQI) completeOperationName( serviceId, @@ -134,18 +134,19 @@ object CompletionProvider { } def completeInQuery( - q: Query[WithSource], - sf: SourceFile[WithSource], + q: playground.generated.nodes.RunQuery, + sf: playground.generated.nodes.SourceFile, ctx: NodeContext, - ): List[CompletionItem] = { + ): List[CompletionItem] = q.operation_name.toList.flatMap { operationName => val resolvedServiceId = MultiServiceResolver - .resolveService( - q.operationName.value, + .resolveServiceTs( + operationName, serviceIndex, - sf.prelude.useClauses.map(_.value), + sf.select(_.prelude.use_clause), ) .toOption + .flatten ctx match { case NodeContext.PathEntry.AtOperationName ^^: EmptyPath => @@ -154,10 +155,12 @@ object CompletionProvider { case NodeContext.PathEntry.AtOperationInput ^^: ctx => resolvedServiceId match { case Some(serviceId) => - inputCompletions(serviceId)( - q.operationName.value.operationName.value.mapK(WithSource.unwrap) - ) - .getCompletions(ctx) + q.select(_.operation_name.name) + .map(id => OperationName[Id](id.source)) + .flatMap { + inputCompletions(serviceId)(_) + .getCompletions(ctx) + } case None => Nil } @@ -169,45 +172,48 @@ object CompletionProvider { ( doc, pos, - ) => - SourceParser[SourceFile].parse(doc) match { - case Left(_) => - // we can try to deal with this later - Nil - - case Right(sf) => - val matchingNode = RangeIndex - .build(sf) - .findAtPosition(pos) - - // System.err.println("matchingNode: " + matchingNode.render) - - matchingNode match { - case NodeContext.PathEntry.InQuery(n) ^^: rest => - val q = - sf - .queries(WithSource.unwrap) - .get(n.toLong) - .getOrElse(sys.error(s"Fatal error: no query at index $n")) - .query - .value - - completeInQuery(q, sf, rest) - - case NodeContext.PathEntry.AtPrelude ^^: - NodeContext.PathEntry.AtUseClause(_) ^^: - EmptyPath => - servicesById - .toList - .sortBy(_._1) - .map(CompletionItem.useServiceClause.tupled) - - case EmptyPath => completeRootOperationName(sf, CompletionItem.InsertBodyStruct.Yes) - - case _ => Nil - } + ) => { + val parsedTs = playground + .generated + .nodes + .SourceFile + .unsafeApply(TreeSitterAPI.make("smithyql").parse(doc).rootNode.get) + + val matchingNode = RangeIndex + .build(parsedTs) + .findAtPosition(pos) + .getOrElse(NodeContext.EmptyPath) + + // System.err.println("matchingNode: " + matchingNode.render) + + matchingNode match { + case NodeContext.PathEntry.InQuery(n) ^^: rest => + val q = parsedTs + .statements + .flatMap(_.run_query) + .get(n.toLong) + .getOrElse(sys.error(s"Fatal error: no query at index $n")) + + completeInQuery(q, parsedTs, rest) + + case NodeContext.PathEntry.AtPrelude ^^: + NodeContext.PathEntry.AtUseClause(_) ^^: + EmptyPath => + servicesById + .toList + .sortBy(_._1) + .map(CompletionItem.useServiceClause.tupled) + + case EmptyPath => + completeRootOperationName( + parsedTs, + CompletionItem.InsertBodyStruct.Yes, + ) + case _ => Nil } + + } } } diff --git a/modules/lsp/src/test/resources/test-workspaces/default/smithy-build.json b/modules/lsp/src/test/resources/test-workspaces/default/smithy-build.json index b0aca2b2..ce803e54 100644 --- a/modules/lsp/src/test/resources/test-workspaces/default/smithy-build.json +++ b/modules/lsp/src/test/resources/test-workspaces/default/smithy-build.json @@ -2,6 +2,6 @@ "sources": ["./weather.smithy", "./no-runner.smithy"], "mavenDependencies": [ "com.disneystreaming.alloy:alloy-core:0.3.14", - "com.disneystreaming.smithy4s:smithy4s-protocol:0.18.26" + "com.disneystreaming.smithy4s:smithy4s-protocol:0.18.27" ] } diff --git a/modules/parser-gen/src/main/scala/playground/parsergen/IR.scala b/modules/parser-gen/src/main/scala/playground/parsergen/IR.scala new file mode 100644 index 00000000..e2c959d7 --- /dev/null +++ b/modules/parser-gen/src/main/scala/playground/parsergen/IR.scala @@ -0,0 +1,50 @@ +package playground.parsergen + +import cats.data.NonEmptyList +import treesittersmithy.FieldName +import treesittersmithy.NodeType +import treesittersmithy.TypeName + +enum Type { + case Union(name: TypeName, subtypes: NonEmptyList[Subtype]) + case Product(name: TypeName, fields: List[Field], children: Option[Children]) +} + +case class Field(name: FieldName, targetTypes: NonEmptyList[TypeName], repeated: Boolean) +case class Children(targetTypes: NonEmptyList[TypeName], repeated: Boolean) + +case class Subtype(name: TypeName) + +object IR { + + def from(nt: NodeType): Type = + if nt.subtypes.nonEmpty then fromUnion(nt) + else + fromProduct(nt) + + private def fromUnion(nt: NodeType): Type.Union = Type.Union( + name = nt.tpe, + subtypes = NonEmptyList.fromListUnsafe(nt.subtypes.map(subtype => Subtype(name = subtype.tpe))), + ) + + private def fromProduct(nt: NodeType): Type.Product = Type.Product( + name = nt.tpe, + fields = + nt.fields + .map { (fieldName, fieldInfo) => + Field( + name = fieldName, + targetTypes = NonEmptyList.fromListUnsafe(fieldInfo.types.map(_.tpe)), + repeated = fieldInfo.multiple, + ) + } + .toList, + children = nt.children.map { children => + Children( + targetTypes = NonEmptyList.fromListUnsafe(children.types.map(_.tpe)), + repeated = children.multiple, + ) + }, + ) + +} diff --git a/modules/parser-gen/src/main/scala/playground/parsergen/ParserGen.scala b/modules/parser-gen/src/main/scala/playground/parsergen/ParserGen.scala new file mode 100644 index 00000000..24061aa0 --- /dev/null +++ b/modules/parser-gen/src/main/scala/playground/parsergen/ParserGen.scala @@ -0,0 +1,373 @@ +package playground.parsergen + +import cats.data.NonEmptyList +import cats.syntax.all.* +import monocle.syntax.all.* +import org.polyvariant.treesitter4s.Node +import smithy4s.Blob +import smithy4s.json.Json +import treesittersmithy.FieldName +import treesittersmithy.NodeType +import treesittersmithy.NodeTypes +import treesittersmithy.TypeName +import util.chaining.* + +import scala.annotation.targetName +import scala.meta.Dialect + +extension (tn: TypeName) { + @targetName("renderTypeName") + def render: String = tn.value.dropWhile(_ == '_').fromSnakeCase.ident + def renderProjection: String = show"as${tn.prettyName}".ident + def renderVisitorMethod: String = show"on${tn.prettyName}".ident + private def prettyName = tn.value.dropWhile(_ == '_').fromSnakeCase + def asChildName: FieldName = FieldName(tn.value) +} + +extension (fn: FieldName) { + @targetName("renderFieldName") + def render: String = fn.value.ident +} + +extension (tpe: NodeType) { + + def render: String = + IR.from(tpe) match { + case union: Type.Union => renderUnion(union) + case product: Type.Product => renderProduct(product) + } + +} + +private def renderUnion(u: Type.Union): String = { + val name = u.name.render + val underlyingType = u.subtypes.map(_.name.render).mkString_(" | ") + + val projections = u.subtypes.map { sub => + // format: off + show"""def ${sub.name.renderProjection}: Option[${sub.name.render}] = ${sub.name.render}.unapply(node)""" + // format: on + } + + val instanceMethods = + show"""extension (node: $name) { + |${projections.mkString_("\n").indentTrim(2)} + | def visit[A](visitor: Visitor[A]): A = visitor.visit(node) + |}""".stripMargin + + val applyMethod = { + val cases = u + .subtypes + .map(nodeType => show"""case ${nodeType.name.render}(node) => Right(node)""") + + show"""def apply(node: Node): Either[String, $name] = node match { + |${cases.mkString_("\n").indentTrim(2)} + | case _ => Left(s"Expected $name, got $${node.tpe}") + |}""".stripMargin + } + + val typedApplyMethod = show"""def apply(node: $underlyingType): $name = node""".stripMargin + + val visitor = + show""" + |trait Visitor[A] { + |${u + .subtypes + .map(sub => show"def ${sub.name.renderVisitorMethod}(node: ${sub.name.render}): A") + .mkString_("\n") + .indentTrim(2)} + | + | def visit(node: $name): A = (node: @nowarn("msg=match may not be exhaustive")) match { + |${u + .subtypes + .map(sub => show"case ${sub.name.render}(node) => ${sub.name.renderVisitorMethod}(node)") + .mkString_("\n") + .indentTrim(4)} + | } + |} + | + |object Visitor { + | abstract class Default[A] extends Visitor[A] { + | def default: A + | + |${u + .subtypes + .map(sub => + show"def ${sub.name.renderVisitorMethod}(node: ${sub.name.render}): A = default" + ) + .mkString_("\n") + .indentTrim(4)} + | } + |} + |""".stripMargin + + val selectorMethods = u + .subtypes + .map { subtype => + // format: off + show"""def ${subtype.name.asChildName.render} : ${subtype.name.render}.Selector = ${subtype.name.render}.Selector(path.flatMap(_.${subtype.name.renderProjection}))""" + // format: on + } + .mkString_("\n") + + show"""// Generated code! Do not modify by hand. + |package playground.generated.nodes + | + |import ${classOf[Node].getName()} + |import playground.treesitter4s.std.Selection + |import annotation.nowarn + | + |opaque type $name <: Node = $underlyingType + | + |object $name { + | + |${instanceMethods.indentTrim(2)} + | + |${applyMethod.indentTrim(2)} + | + |${typedApplyMethod.indentTrim(2)} + | + | def unsafeApply(node: Node): $name = apply(node).fold(sys.error, identity) + | + | def unapply(node: Node): Option[$name] = apply(node).toOption + | + |${visitor.indentTrim(2)} + | + | final case class Selector(path: List[$name]) extends Selection[$name] { + |${selectorMethods.indentTrim(4)} + | + | type Self = Selector + | protected val remake = Selector.apply + | } + |} + |""".stripMargin +} + +private def renderProduct(p: Type.Product): String = { + val name = p.name.render + + def renderTypeUnion(types: NonEmptyList[TypeName]) = types + .map(_.render) + .reduceLeft(_ + " | " + _) + + def renderFieldType(field: Field): String = renderTypeUnion(field.targetTypes).pipe { + case s if field.repeated => show"List[$s]" + case s => show"Option[$s]" + } + + def renderChildrenType(children: Children): String = renderTypeUnion(children.targetTypes).pipe { + case s if children.repeated => show"List[$s]" + case s => show"Option[$s]" + } + + def renderChildType(tpe: TypeName, repeated: Boolean): String = tpe.render.pipe { + case s if repeated => show"List[$s]" + case s => show"Option[$s]" + } + + val fieldGetters = p + .fields + .map { field => + val allFields = show"""node.fields.getOrElse(${field.name.value.literal}, Nil)""" + + val cases = field.targetTypes.map { tpe => + show"""case ${tpe.render}(node) => node""" + } + + val fieldValue = + if field.repeated then show"""$allFields.toList.collect { + |${cases.mkString_("\n").indentTrim(2)} + |}""".stripMargin + else + show"""$allFields.headOption.map { + |${cases.mkString_("\n").indentTrim(2)} + |}""".stripMargin + + show"""def ${field.name.render}: ${renderFieldType(field)} = $fieldValue""" + } + + val typedChildren = p.children.map { children => + val fieldTypeAnnotation = renderChildrenType(children) + + val allChildren = show"""node.children""" + + val cases = children.targetTypes.map { tpe => + show"""case ${tpe.render}(node) => node""" + } + + val fieldValue = + if children.repeated then show"""$allChildren.toList.collect { + |${cases.mkString_("\n").indentTrim(2)} + |}""".stripMargin + else + show"""$allChildren.collectFirst { + |${cases.mkString_("\n").indentTrim(2)} + |}""".stripMargin + + show"""def typedChildren: ${fieldTypeAnnotation} = $fieldValue""" + } + + val typedChildrenPrecise = p + .children + .toList + .flatMap { fieldInfo => + fieldInfo.targetTypes.map((fieldInfo.repeated, _)).toList + } + .map { (repeated, fieldType) => + val fieldTypeAnnotation = renderChildType(fieldType, repeated) + val childValue = + if repeated then show"""node.children.toList.collect { + | case ${fieldType.render}(node) => node + |}""".stripMargin + else + show"""node.children.collectFirst { + | case ${fieldType.render}(node) => node + |}""".stripMargin + + show"""def ${fieldType.asChildName.render}: $fieldTypeAnnotation = $childValue""".stripMargin + } + + val instanceMethods = + if (fieldGetters.nonEmpty || typedChildren.nonEmpty || typedChildrenPrecise.nonEmpty) { + show"""extension (node: $name) { + | def select[A](f: $name.Selector => Selection[A]): List[A] = f($name.Selector(List(node))).path + | // fields + |${fieldGetters.mkString_("\n\n").indentTrim(2)} + | // typed children + |${typedChildren.foldMap(_.indentTrim(2)): String} + | // precise typed children + |${typedChildrenPrecise.mkString_("\n\n").indentTrim(2)} + |}""".stripMargin + } else + "" + + val selectorMethods = p + .fields + .flatMap { + case field if field.targetTypes.size == 1 => + // format: off + show"""def ${field.name.render}: ${field.targetTypes.head.render}.Selector = ${field.targetTypes.head.render}.Selector(path.flatMap(_.${field.name.render}))""".stripMargin.some + // format: on + + case f => + System + .err + .println( + s"Skipping selector for field ${f.name} in product $name as it has multiple target types" + ) + none + } + .concat( + p.children.toList.flatMap(_.targetTypes.toList).map { tpe => + // format: off + show"""def ${tpe.asChildName.render}: ${tpe.render}.Selector = ${tpe.render}.Selector(path.flatMap(_.${tpe.asChildName.render}))""".stripMargin + // format: on + } + ) + .mkString_("\n") + + show"""// Generated code! Do not modify by hand. + |package playground.generated.nodes + | + |import ${classOf[Node].getName()} + |import playground.treesitter4s.std.Selection + | + |opaque type $name <: Node = Node + | + |object $name { + |${instanceMethods.indentTrim(2)} + | + | def apply(node: Node): Either[String, $name] = + | if node.tpe == ${p.name.value.literal} + | then Right(node) + | else Left(s"Expected ${p.name.render}, got $${node.tpe}") + | + | def unsafeApply(node: Node): $name = apply(node).fold(sys.error, identity) + | + | def unapply(node: Node): Option[$name] = apply(node).toOption + | + | final case class Selector(path: List[$name]) extends Selection[$name] { + |${selectorMethods.indentTrim(4)} + | + | type Self = Selector + | protected val remake = Selector.apply + | } + |} + |""".stripMargin + +} + +@main def parserGen = { + val types = + Json + .read[NodeTypes]( + Blob(os.read(os.pwd / "tree-sitter-smithyql" / "src" / "node-types.json")) + ) + .toTry + .get + .value + + val base = + os.pwd / "modules" / "treesitter" / "src" / "main" / "scala" / "playground" / "generated" / "nodes" + + val rendered = types + .filter(_.named) + .map( + // only render field types that are named + _.focus(_.fields.each.types) + .modify(_.filter(_.named)) + // don't render the field if it has no types + .focus(_.fields) + .modify(_.filter((_, v) => v.types.nonEmpty)) + ) + .fproduct( + _.render + ) + + os.remove.all(base) + + rendered + .foreach { (tpe, code) => + os.write( + base / s"${tpe.tpe.render}.scala", + code, + createFolders = true, + ) + } +} + +extension (s: String) { + + def indentTrim(n: Int): String = s + .linesIterator + .map { + case line if line.nonEmpty => " " * n + line + case line => line + } + .mkString("\n") + + def trimLines: String = s.linesIterator.map(_.stripTrailing()).mkString("\n") + + def literal: String = scala.meta.Lit.String(s).printSyntaxFor(scala.meta.dialects.Scala3) + + def ident: String = { + // etc. + val reserved = Set("List", "String", "Boolean", "Null") + if reserved(s) then s + "_" + else + scala.meta.Name(s).printSyntaxFor(scala.meta.dialects.Scala3) + } + + def fromSnakeCase: String = s.split('_').map(_.capitalize).mkString + +} + +extension [A](l: List[A]) { + + def requireOnly: A = + l match { + case a :: Nil => a + case _ => throw new IllegalArgumentException(s"Expected exactly one element, got $l") + } + +} diff --git a/modules/parser-gen/src/main/smithy/treesitter.smithy b/modules/parser-gen/src/main/smithy/treesitter.smithy new file mode 100644 index 00000000..3c7c687e --- /dev/null +++ b/modules/parser-gen/src/main/smithy/treesitter.smithy @@ -0,0 +1,58 @@ +$version: "2" + +namespace treesittersmithy + +list NodeTypes { + member: NodeType +} + +structure NodeType { + @required + @jsonName("type") + tpe: TypeName + + @required + named: Boolean + + @required + fields: NodeFields = {} + + children: FieldInfo + + @required + subtypes: NodeTypes = [] +} + +string TypeName + +map NodeFields { + key: FieldName + value: FieldInfo +} + +string FieldName + +structure FieldInfo { + @required + multiple: Boolean + + @required + required: Boolean + + @required + types: TypeList +} + +list TypeList { + member: TypeInfo +} + +// https://github.com/disneystreaming/smithy4s/issues/1618 +structure TypeInfo { + @required + @jsonName("type") + tpe: TypeName + + @required + named: Boolean +} diff --git a/modules/parser/src/test/scala/playground/smithyql/parser/ParserSuite.scala b/modules/parser/src/test/scala/playground/smithyql/parser/ParserSuite.scala index 95ea1188..9b15eda0 100644 --- a/modules/parser/src/test/scala/playground/smithyql/parser/ParserSuite.scala +++ b/modules/parser/src/test/scala/playground/smithyql/parser/ParserSuite.scala @@ -8,6 +8,8 @@ import fs2.io.file.Path import io.circe.Codec import io.circe.Decoder import io.circe.syntax.* +import org.polyvariant.treesitter4s.Node +import org.polyvariant.treesitter4s.TreeSitterAPI import playground.Assertions.* import playground.smithyql.* import playground.smithyql.parser.v2.scanner.Scanner @@ -20,6 +22,8 @@ import java.nio.file.Paths trait ParserSuite extends SimpleIOSuite { + def treeSitterWrap(fileSource: String): String = fileSource + def loadParserTests[Alg[_[_]]: SourceParser]( prefix: String, // this isn't on by default because whitespace in full files (currently, 1-1 mapping to queries) is significant and should not be trimmed before parsing. @@ -56,6 +60,7 @@ trait ParserSuite extends SimpleIOSuite { } validTokensTest(testCase, trimWhitespace) + treeSitterTest(testCase, trimWhitespace) } private def validTokensTest( @@ -73,6 +78,27 @@ trait ParserSuite extends SimpleIOSuite { } } + private def treeSitterTest( + testCase: TestCase, + trimWhitespace: Boolean, + ) = + test(testCase.name + " (tree-sitter no errors)") { + testCase.readInput(trimWhitespace).map { input => + val src = treeSitterWrap(input) + val scanned = TreeSitterAPI.make("smithyql").parse(src).rootNode.get + + val errors = scanned + .fold[List[Node]](_ :: _.flatten.toList) + .filter(_.isError) + .map { node => + s"${node.source} (${node.startByte} to ${node.endByte}, ${node.selfAndParents.map(_.tpe).mkString(" -> ")})" + } + .mkString("\n") + + assert(errors.isEmpty) || failure("error in file: " + testCase.base) || failure(src) + } + } + // invalidTokens: a flag that tells the suite whether the file should contain invalid tokens. def loadNegativeParserTests[Alg[_[_]]: SourceParser]( prefix: String, @@ -91,8 +117,23 @@ trait ParserSuite extends SimpleIOSuite { if (!invalidTokens) validTokensTest(testCase, trimWhitespace) + treeSitterNegativeTest(testCase, trimWhitespace) } + private def treeSitterNegativeTest( + testCase: TestCase, + trimWhitespace: Boolean, + ) = + test(testCase.name + " (tree-sitter require errors)") { + testCase.readInput(trimWhitespace).map { input => + val scanned = TreeSitterAPI.make("smithyql").parse(input).rootNode.get + + val errors = scanned.fold[List[Node]](_ :: _.flatten.toList).find(_.isError) + + assert(errors.nonEmpty) + } + } + private def readText( path: Path ) = @@ -144,11 +185,12 @@ trait ParserSuite extends SimpleIOSuite { outputExtension: String, ) { + private val inputPath = base / "input.smithyql-test" private val outputPath = base / s"output$outputExtension" def readInput( trimWhitespace: Boolean - ): IO[String] = readText(base / "input.smithyql-test").map( + ): IO[String] = readText(inputPath).map( if (trimWhitespace) _.strip else diff --git a/modules/parser/src/test/scala/playground/smithyql/parser/generative/ListParserTests.scala b/modules/parser/src/test/scala/playground/smithyql/parser/generative/ListParserTests.scala index 8458527b..f70cf43f 100644 --- a/modules/parser/src/test/scala/playground/smithyql/parser/generative/ListParserTests.scala +++ b/modules/parser/src/test/scala/playground/smithyql/parser/generative/ListParserTests.scala @@ -7,5 +7,11 @@ import playground.smithyql.parser.ParserSuite import playground.smithyql.parser.SourceParser object ListParserTests extends ParserSuite { + + override def treeSitterWrap(fileSource: String): String = + s"""FakeCall { + | fakeField = $fileSource + |}""".stripMargin + loadParserTests[Listed]("listed", trimWhitespace = true) } diff --git a/modules/parser/src/test/scala/playground/smithyql/parser/generative/StructParserTests.scala b/modules/parser/src/test/scala/playground/smithyql/parser/generative/StructParserTests.scala index 1eebed2f..9aba2774 100644 --- a/modules/parser/src/test/scala/playground/smithyql/parser/generative/StructParserTests.scala +++ b/modules/parser/src/test/scala/playground/smithyql/parser/generative/StructParserTests.scala @@ -6,5 +6,7 @@ import playground.smithyql.parser.Codecs.given import playground.smithyql.parser.ParserSuite object StructParserTests extends ParserSuite { + + override def treeSitterWrap(fileSource: String): String = s"FakeCall $fileSource" loadParserTests[Struct]("struct", trimWhitespace = true) } diff --git a/modules/treesitter/src/main/resources/darwin-aarch64/libtree-sitter-smithyql.dylib b/modules/treesitter/src/main/resources/darwin-aarch64/libtree-sitter-smithyql.dylib new file mode 100755 index 00000000..0c4dce8f Binary files /dev/null and b/modules/treesitter/src/main/resources/darwin-aarch64/libtree-sitter-smithyql.dylib differ diff --git a/modules/treesitter/src/main/resources/darwin-x86-64/libtree-sitter-smithyql.dylib b/modules/treesitter/src/main/resources/darwin-x86-64/libtree-sitter-smithyql.dylib new file mode 100755 index 00000000..a57fb005 Binary files /dev/null and b/modules/treesitter/src/main/resources/darwin-x86-64/libtree-sitter-smithyql.dylib differ diff --git a/modules/treesitter/src/main/resources/linux-aarch64/libtree-sitter-smithyql.so b/modules/treesitter/src/main/resources/linux-aarch64/libtree-sitter-smithyql.so new file mode 100755 index 00000000..9c44704b Binary files /dev/null and b/modules/treesitter/src/main/resources/linux-aarch64/libtree-sitter-smithyql.so differ diff --git a/modules/treesitter/src/main/resources/linux-x86-64/libtree-sitter-smithyql.so b/modules/treesitter/src/main/resources/linux-x86-64/libtree-sitter-smithyql.so new file mode 100755 index 00000000..7a4b2389 Binary files /dev/null and b/modules/treesitter/src/main/resources/linux-x86-64/libtree-sitter-smithyql.so differ diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Binding.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Binding.scala new file mode 100644 index 00000000..ecfb9bfd --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Binding.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Binding <: Node = Node + +object Binding { + extension (node: Binding) { + def select[A](f: Binding.Selector => Selection[A]): List[A] = f(Binding.Selector(List(node))).path + // fields + def key: Option[Identifier] = node.fields.getOrElse("key", Nil).headOption.map { + case Identifier(node) => node + } + + def value: Option[InputNode] = node.fields.getOrElse("value", Nil).headOption.map { + case InputNode(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, Binding] = + if node.tpe == "binding" + then Right(node) + else Left(s"Expected Binding, got ${node.tpe}") + + def unsafeApply(node: Node): Binding = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Binding] = apply(node).toOption + + final case class Selector(path: List[Binding]) extends Selection[Binding] { + def key: Identifier.Selector = Identifier.Selector(path.flatMap(_.key)) + def value: InputNode.Selector = InputNode.Selector(path.flatMap(_.value)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Boolean_.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Boolean_.scala new file mode 100644 index 00000000..295034fc --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Boolean_.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Boolean_ <: Node = Node + +object Boolean_ { + + + def apply(node: Node): Either[String, Boolean_] = + if node.tpe == "boolean" + then Right(node) + else Left(s"Expected Boolean_, got ${node.tpe}") + + def unsafeApply(node: Node): Boolean_ = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Boolean_] = apply(node).toOption + + final case class Selector(path: List[Boolean_]) extends Selection[Boolean_] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Comment.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Comment.scala new file mode 100644 index 00000000..936272cf --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Comment.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Comment <: Node = Node + +object Comment { + + + def apply(node: Node): Either[String, Comment] = + if node.tpe == "comment" + then Right(node) + else Left(s"Expected Comment, got ${node.tpe}") + + def unsafeApply(node: Node): Comment = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Comment] = apply(node).toOption + + final case class Selector(path: List[Comment]) extends Selection[Comment] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Identifier.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Identifier.scala new file mode 100644 index 00000000..e942fe9b --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Identifier.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Identifier <: Node = Node + +object Identifier { + + + def apply(node: Node): Either[String, Identifier] = + if node.tpe == "identifier" + then Right(node) + else Left(s"Expected Identifier, got ${node.tpe}") + + def unsafeApply(node: Node): Identifier = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Identifier] = apply(node).toOption + + final case class Selector(path: List[Identifier]) extends Selection[Identifier] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/InputNode.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/InputNode.scala new file mode 100644 index 00000000..41c64fdc --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/InputNode.scala @@ -0,0 +1,81 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection +import annotation.nowarn + +opaque type InputNode <: Node = Boolean_ | List_ | Null_ | Number | String_ | Struct + +object InputNode { + + extension (node: InputNode) { + def asBoolean: Option[Boolean_] = Boolean_.unapply(node) + def asList: Option[List_] = List_.unapply(node) + def asNull: Option[Null_] = Null_.unapply(node) + def asNumber: Option[Number] = Number.unapply(node) + def asString: Option[String_] = String_.unapply(node) + def asStruct: Option[Struct] = Struct.unapply(node) + def visit[A](visitor: Visitor[A]): A = visitor.visit(node) + } + + def apply(node: Node): Either[String, InputNode] = node match { + case Boolean_(node) => Right(node) + case List_(node) => Right(node) + case Null_(node) => Right(node) + case Number(node) => Right(node) + case String_(node) => Right(node) + case Struct(node) => Right(node) + case _ => Left(s"Expected InputNode, got ${node.tpe}") + } + + def apply(node: Boolean_ | List_ | Null_ | Number | String_ | Struct): InputNode = node + + def unsafeApply(node: Node): InputNode = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[InputNode] = apply(node).toOption + + + trait Visitor[A] { + def onBoolean(node: Boolean_): A + def onList(node: List_): A + def onNull(node: Null_): A + def onNumber(node: Number): A + def onString(node: String_): A + def onStruct(node: Struct): A + + def visit(node: InputNode): A = (node: @nowarn("msg=match may not be exhaustive")) match { + case Boolean_(node) => onBoolean(node) + case List_(node) => onList(node) + case Null_(node) => onNull(node) + case Number(node) => onNumber(node) + case String_(node) => onString(node) + case Struct(node) => onStruct(node) + } + } + + object Visitor { + abstract class Default[A] extends Visitor[A] { + def default: A + + def onBoolean(node: Boolean_): A = default + def onList(node: List_): A = default + def onNull(node: Null_): A = default + def onNumber(node: Number): A = default + def onString(node: String_): A = default + def onStruct(node: Struct): A = default + } + } + + final case class Selector(path: List[InputNode]) extends Selection[InputNode] { + def boolean : Boolean_.Selector = Boolean_.Selector(path.flatMap(_.asBoolean)) + def list : List_.Selector = List_.Selector(path.flatMap(_.asList)) + def `null` : Null_.Selector = Null_.Selector(path.flatMap(_.asNull)) + def number : Number.Selector = Number.Selector(path.flatMap(_.asNumber)) + def string : String_.Selector = String_.Selector(path.flatMap(_.asString)) + def struct : Struct.Selector = Struct.Selector(path.flatMap(_.asStruct)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/List_.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/List_.scala new file mode 100644 index 00000000..679214ff --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/List_.scala @@ -0,0 +1,37 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type List_ <: Node = Node + +object List_ { + extension (node: List_) { + def select[A](f: List_.Selector => Selection[A]): List[A] = f(List_.Selector(List(node))).path + // fields + def list_fields: List[InputNode] = node.fields.getOrElse("list_fields", Nil).toList.collect { + case InputNode(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, List_] = + if node.tpe == "list" + then Right(node) + else Left(s"Expected List_, got ${node.tpe}") + + def unsafeApply(node: Node): List_ = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[List_] = apply(node).toOption + + final case class Selector(path: List[List_]) extends Selection[List_] { + def list_fields: InputNode.Selector = InputNode.Selector(path.flatMap(_.list_fields)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Null_.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Null_.scala new file mode 100644 index 00000000..5e438bb4 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Null_.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Null_ <: Node = Node + +object Null_ { + + + def apply(node: Node): Either[String, Null_] = + if node.tpe == "null" + then Right(node) + else Left(s"Expected Null_, got ${node.tpe}") + + def unsafeApply(node: Node): Null_ = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Null_] = apply(node).toOption + + final case class Selector(path: List[Null_]) extends Selection[Null_] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Number.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Number.scala new file mode 100644 index 00000000..66014bb1 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Number.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Number <: Node = Node + +object Number { + + + def apply(node: Node): Either[String, Number] = + if node.tpe == "number" + then Right(node) + else Left(s"Expected Number, got ${node.tpe}") + + def unsafeApply(node: Node): Number = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Number] = apply(node).toOption + + final case class Selector(path: List[Number]) extends Selection[Number] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/OperationName.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/OperationName.scala new file mode 100644 index 00000000..f5ad7ad4 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/OperationName.scala @@ -0,0 +1,39 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type OperationName <: Node = Node + +object OperationName { + extension (node: OperationName) { + def select[A](f: OperationName.Selector => Selection[A]): List[A] = f(OperationName.Selector(List(node))).path + // fields + + // typed children + def typedChildren: Option[Identifier] = node.children.collectFirst { + case Identifier(node) => node + } + // precise typed children + def identifier: Option[Identifier] = node.children.collectFirst { + case Identifier(node) => node + } + } + + def apply(node: Node): Either[String, OperationName] = + if node.tpe == "operation_name" + then Right(node) + else Left(s"Expected OperationName, got ${node.tpe}") + + def unsafeApply(node: Node): OperationName = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[OperationName] = apply(node).toOption + + final case class Selector(path: List[OperationName]) extends Selection[OperationName] { + def identifier: Identifier.Selector = Identifier.Selector(path.flatMap(_.identifier)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Prelude.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Prelude.scala new file mode 100644 index 00000000..f5fd7dbd --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Prelude.scala @@ -0,0 +1,39 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Prelude <: Node = Node + +object Prelude { + extension (node: Prelude) { + def select[A](f: Prelude.Selector => Selection[A]): List[A] = f(Prelude.Selector(List(node))).path + // fields + + // typed children + def typedChildren: List[UseClause] = node.children.toList.collect { + case UseClause(node) => node + } + // precise typed children + def use_clause: List[UseClause] = node.children.toList.collect { + case UseClause(node) => node + } + } + + def apply(node: Node): Either[String, Prelude] = + if node.tpe == "prelude" + then Right(node) + else Left(s"Expected Prelude, got ${node.tpe}") + + def unsafeApply(node: Node): Prelude = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Prelude] = apply(node).toOption + + final case class Selector(path: List[Prelude]) extends Selection[Prelude] { + def use_clause: UseClause.Selector = UseClause.Selector(path.flatMap(_.use_clause)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/QualifiedIdentifier.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/QualifiedIdentifier.scala new file mode 100644 index 00000000..0cde7e20 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/QualifiedIdentifier.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type QualifiedIdentifier <: Node = Node + +object QualifiedIdentifier { + extension (node: QualifiedIdentifier) { + def select[A](f: QualifiedIdentifier.Selector => Selection[A]): List[A] = f(QualifiedIdentifier.Selector(List(node))).path + // fields + def namespace: List[Identifier] = node.fields.getOrElse("namespace", Nil).toList.collect { + case Identifier(node) => node + } + + def selection: Option[Identifier] = node.fields.getOrElse("selection", Nil).headOption.map { + case Identifier(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, QualifiedIdentifier] = + if node.tpe == "qualified_identifier" + then Right(node) + else Left(s"Expected QualifiedIdentifier, got ${node.tpe}") + + def unsafeApply(node: Node): QualifiedIdentifier = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[QualifiedIdentifier] = apply(node).toOption + + final case class Selector(path: List[QualifiedIdentifier]) extends Selection[QualifiedIdentifier] { + def namespace: Identifier.Selector = Identifier.Selector(path.flatMap(_.namespace)) + def selection: Identifier.Selector = Identifier.Selector(path.flatMap(_.selection)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/QueryOperationName.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/QueryOperationName.scala new file mode 100644 index 00000000..9480e227 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/QueryOperationName.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type QueryOperationName <: Node = Node + +object QueryOperationName { + extension (node: QueryOperationName) { + def select[A](f: QueryOperationName.Selector => Selection[A]): List[A] = f(QueryOperationName.Selector(List(node))).path + // fields + def name: Option[OperationName] = node.fields.getOrElse("name", Nil).headOption.map { + case OperationName(node) => node + } + + def service_identifier: Option[QualifiedIdentifier] = node.fields.getOrElse("service_identifier", Nil).headOption.map { + case QualifiedIdentifier(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, QueryOperationName] = + if node.tpe == "query_operation_name" + then Right(node) + else Left(s"Expected QueryOperationName, got ${node.tpe}") + + def unsafeApply(node: Node): QueryOperationName = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[QueryOperationName] = apply(node).toOption + + final case class Selector(path: List[QueryOperationName]) extends Selection[QueryOperationName] { + def name: OperationName.Selector = OperationName.Selector(path.flatMap(_.name)) + def service_identifier: QualifiedIdentifier.Selector = QualifiedIdentifier.Selector(path.flatMap(_.service_identifier)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/RunQuery.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/RunQuery.scala new file mode 100644 index 00000000..fb741774 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/RunQuery.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type RunQuery <: Node = Node + +object RunQuery { + extension (node: RunQuery) { + def select[A](f: RunQuery.Selector => Selection[A]): List[A] = f(RunQuery.Selector(List(node))).path + // fields + def input: Option[Struct] = node.fields.getOrElse("input", Nil).headOption.map { + case Struct(node) => node + } + + def operation_name: Option[QueryOperationName] = node.fields.getOrElse("operation_name", Nil).headOption.map { + case QueryOperationName(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, RunQuery] = + if node.tpe == "run_query" + then Right(node) + else Left(s"Expected RunQuery, got ${node.tpe}") + + def unsafeApply(node: Node): RunQuery = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[RunQuery] = apply(node).toOption + + final case class Selector(path: List[RunQuery]) extends Selection[RunQuery] { + def input: Struct.Selector = Struct.Selector(path.flatMap(_.input)) + def operation_name: QueryOperationName.Selector = QueryOperationName.Selector(path.flatMap(_.operation_name)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/SourceFile.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/SourceFile.scala new file mode 100644 index 00000000..dba19b71 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/SourceFile.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type SourceFile <: Node = Node + +object SourceFile { + extension (node: SourceFile) { + def select[A](f: SourceFile.Selector => Selection[A]): List[A] = f(SourceFile.Selector(List(node))).path + // fields + def prelude: Option[Prelude] = node.fields.getOrElse("prelude", Nil).headOption.map { + case Prelude(node) => node + } + + def statements: List[TopLevelStatement] = node.fields.getOrElse("statements", Nil).toList.collect { + case TopLevelStatement(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, SourceFile] = + if node.tpe == "source_file" + then Right(node) + else Left(s"Expected SourceFile, got ${node.tpe}") + + def unsafeApply(node: Node): SourceFile = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[SourceFile] = apply(node).toOption + + final case class Selector(path: List[SourceFile]) extends Selection[SourceFile] { + def prelude: Prelude.Selector = Prelude.Selector(path.flatMap(_.prelude)) + def statements: TopLevelStatement.Selector = TopLevelStatement.Selector(path.flatMap(_.statements)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/String_.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/String_.scala new file mode 100644 index 00000000..dc03603d --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/String_.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type String_ <: Node = Node + +object String_ { + + + def apply(node: Node): Either[String, String_] = + if node.tpe == "string" + then Right(node) + else Left(s"Expected String_, got ${node.tpe}") + + def unsafeApply(node: Node): String_ = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[String_] = apply(node).toOption + + final case class Selector(path: List[String_]) extends Selection[String_] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Struct.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Struct.scala new file mode 100644 index 00000000..5523d639 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Struct.scala @@ -0,0 +1,37 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Struct <: Node = Node + +object Struct { + extension (node: Struct) { + def select[A](f: Struct.Selector => Selection[A]): List[A] = f(Struct.Selector(List(node))).path + // fields + def bindings: List[Binding] = node.fields.getOrElse("bindings", Nil).toList.collect { + case Binding(node) => node + } + // typed children + + // precise typed children + + } + + def apply(node: Node): Either[String, Struct] = + if node.tpe == "struct" + then Right(node) + else Left(s"Expected Struct, got ${node.tpe}") + + def unsafeApply(node: Node): Struct = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Struct] = apply(node).toOption + + final case class Selector(path: List[Struct]) extends Selection[Struct] { + def bindings: Binding.Selector = Binding.Selector(path.flatMap(_.bindings)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/TopLevelStatement.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/TopLevelStatement.scala new file mode 100644 index 00000000..26f9468f --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/TopLevelStatement.scala @@ -0,0 +1,39 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type TopLevelStatement <: Node = Node + +object TopLevelStatement { + extension (node: TopLevelStatement) { + def select[A](f: TopLevelStatement.Selector => Selection[A]): List[A] = f(TopLevelStatement.Selector(List(node))).path + // fields + + // typed children + def typedChildren: Option[RunQuery] = node.children.collectFirst { + case RunQuery(node) => node + } + // precise typed children + def run_query: Option[RunQuery] = node.children.collectFirst { + case RunQuery(node) => node + } + } + + def apply(node: Node): Either[String, TopLevelStatement] = + if node.tpe == "top_level_statement" + then Right(node) + else Left(s"Expected TopLevelStatement, got ${node.tpe}") + + def unsafeApply(node: Node): TopLevelStatement = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[TopLevelStatement] = apply(node).toOption + + final case class Selector(path: List[TopLevelStatement]) extends Selection[TopLevelStatement] { + def run_query: RunQuery.Selector = RunQuery.Selector(path.flatMap(_.run_query)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/UseClause.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/UseClause.scala new file mode 100644 index 00000000..6d7c13b2 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/UseClause.scala @@ -0,0 +1,42 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type UseClause <: Node = Node + +object UseClause { + extension (node: UseClause) { + def select[A](f: UseClause.Selector => Selection[A]): List[A] = f(UseClause.Selector(List(node))).path + // fields + def identifier: Option[QualifiedIdentifier] = node.fields.getOrElse("identifier", Nil).headOption.map { + case QualifiedIdentifier(node) => node + } + // typed children + def typedChildren: List[Whitespace] = node.children.toList.collect { + case Whitespace(node) => node + } + // precise typed children + def whitespace: List[Whitespace] = node.children.toList.collect { + case Whitespace(node) => node + } + } + + def apply(node: Node): Either[String, UseClause] = + if node.tpe == "use_clause" + then Right(node) + else Left(s"Expected UseClause, got ${node.tpe}") + + def unsafeApply(node: Node): UseClause = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[UseClause] = apply(node).toOption + + final case class Selector(path: List[UseClause]) extends Selection[UseClause] { + def identifier: QualifiedIdentifier.Selector = QualifiedIdentifier.Selector(path.flatMap(_.identifier)) + def whitespace: Whitespace.Selector = Whitespace.Selector(path.flatMap(_.whitespace)) + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/generated/nodes/Whitespace.scala b/modules/treesitter/src/main/scala/playground/generated/nodes/Whitespace.scala new file mode 100644 index 00000000..ee663890 --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/generated/nodes/Whitespace.scala @@ -0,0 +1,27 @@ +// Generated code! Do not modify by hand. +package playground.generated.nodes + +import org.polyvariant.treesitter4s.Node +import playground.treesitter4s.std.Selection + +opaque type Whitespace <: Node = Node + +object Whitespace { + + + def apply(node: Node): Either[String, Whitespace] = + if node.tpe == "whitespace" + then Right(node) + else Left(s"Expected Whitespace, got ${node.tpe}") + + def unsafeApply(node: Node): Whitespace = apply(node).fold(sys.error, identity) + + def unapply(node: Node): Option[Whitespace] = apply(node).toOption + + final case class Selector(path: List[Whitespace]) extends Selection[Whitespace] { + + + type Self = Selector + protected val remake = Selector.apply + } +} diff --git a/modules/treesitter/src/main/scala/playground/treesitter4s/std/Selection.scala b/modules/treesitter/src/main/scala/playground/treesitter4s/std/Selection.scala new file mode 100644 index 00000000..fbf2016a --- /dev/null +++ b/modules/treesitter/src/main/scala/playground/treesitter4s/std/Selection.scala @@ -0,0 +1,10 @@ +package playground.treesitter4s.std + +trait Selection[A] { + type Self <: Selection[A] + def path: List[A] + protected def remake: List[A] => Self + + def transform(f: List[A] => List[A]): Self = remake(f(path)) + def find(f: A => Boolean): Self = transform(_.find(f).toList) +} diff --git a/modules/treesitter/src/test/scala/playground/smithyql/parser/v3/TreeSitterParserTests.scala b/modules/treesitter/src/test/scala/playground/smithyql/parser/v3/TreeSitterParserTests.scala new file mode 100644 index 00000000..10900b12 --- /dev/null +++ b/modules/treesitter/src/test/scala/playground/smithyql/parser/v3/TreeSitterParserTests.scala @@ -0,0 +1,96 @@ +package playground.smithyql.parser.v3 + +import org.polyvariant.treesitter4s.Node +import org.polyvariant.treesitter4s.TreeSitterAPI +import playground.generated.nodes.SourceFile +import weaver.* + +object TreeSitterParserTests extends FunSuite { + + private def parse(s: String): SourceFile = { + val p = TreeSitterAPI.make("smithyql") + SourceFile.unsafeApply(p.parse(s).rootNode.get) + } + + test("SourceFile fields") { + val in = parse("""use service foo.bar.baz.bax#Baz + |GetBaz{}""".stripMargin) + + assert.eql(in.prelude.map(_.use_clause.size), Some(1)) && + assert(in.statements.nonEmpty) + } + + test("All parents of deep child") { + val allNodes = parse("""use service foo.bar.baz.bax#Baz + |GetBaz { a = { x = 42 }}""".stripMargin) + .fold[List[Node]](_ :: _.flatten.toList) + + val parentTypesAndSources = allNodes + .find(_.source == "x") + .get + .selfAndParents + .map(n => n.tpe -> n.source) + .mkString("\n") + + val expected = List( + "identifier" -> "x", + "binding" -> "x = 42", + "struct" -> "{ x = 42 }", + "binding" -> "a = { x = 42 }", + "struct" -> "{ a = { x = 42 }}", + "run_query" -> "GetBaz { a = { x = 42 }}", + "top_level_statement" -> "GetBaz { a = { x = 42 }}", + "source_file" -> "use service foo.bar.baz.bax#Baz\nGetBaz { a = { x = 42 }}", + ).mkString("\n") + + assert.same(expected, parentTypesAndSources) + } + + test("Deep insight into field") { + val in = parse("""use service foo.bar.baz.bax#Baz + |GetBaz { a = { x = 42 } }""".stripMargin) + + val valueOfX = + in.select( + _.statements + .run_query + .input + .bindings + .find(_.key.get.source == "a") + .value + .struct + .bindings + .find(_.key.get.source == "x") + .value + .number + ).head + .source + .toInt + + assert.eql(42, valueOfX) + } + + test("Deep insight into field, but the file isn't valid") { + val in = parse("""use service fo o.b ar.b/az.bax/#//B//,,{}az + |GetBa z { a = { x = 42, 50 }, z, 42 }""".stripMargin) + + val valueOfX = + in.select( + _.statements + .run_query + .input + .bindings + .find(_.key.get.source == "a") + .value + .struct + .bindings + .find(_.key.get.source == "x") + .value + .number + ).head + .source + .toInt + + assert.eql(42, valueOfX) + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index b4014537..2e68aa92 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.11") addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.2") // try to keep in sync with smithy-build.json -addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.18.25") +addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.18.27") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.1") diff --git a/smithy-build.json b/smithy-build.json index 4364f960..69763f2e 100644 --- a/smithy-build.json +++ b/smithy-build.json @@ -1,8 +1,20 @@ { - "sources": ["modules/examples/src/main/smithy"], - "mavenDependencies": [ - "com.disneystreaming.alloy:alloy-core:0.3.14", - "com.disneystreaming.smithy4s:smithy4s-protocol:0.18.26", - "software.amazon.smithy:smithy-aws-traits:1.53.0" - ] + "version": "1.0", + "sources": [ + "modules/examples/src/main/smithy", + "modules/parser-gen/src/main/smithy", + "modules/parser-gen/target/scala-3.5.2/src_managed/main/smithy", + "modules/examples/target/scala-3.5.2/src_managed/main/smithy", + "modules/protocol4s/src/main/smithy", + "modules/protocol4s/target/scala-3.5.2/src_managed/main/smithy" + ], + "maven": { + "dependencies": [ + "com.disneystreaming.alloy:alloy-core:0.3.14", + "com.disneystreaming.smithy4s:smithy4s-protocol:0.18.27", + "software.amazon.smithy:smithy-aws-traits:1.53.0" + ], + + "repositories": [] + } } diff --git a/tree-sitter-smithyql/.gitattributes b/tree-sitter-smithyql/.gitattributes new file mode 100644 index 00000000..4cb10583 --- /dev/null +++ b/tree-sitter-smithyql/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +src/*.json linguist-generated +src/parser.c linguist-generated +src/tree_sitter/* linguist-generated + +bindings/** linguist-generated +binding.gyp linguist-generated +setup.py linguist-generated +Makefile linguist-generated +Package.swift linguist-generated diff --git a/tree-sitter-smithyql/.gitignore b/tree-sitter-smithyql/.gitignore new file mode 100644 index 00000000..5d11846d --- /dev/null +++ b/tree-sitter-smithyql/.gitignore @@ -0,0 +1,54 @@ +# Rust artifacts +Cargo.lock +target/ + +# Node artifacts +build/ +prebuilds/ +node_modules/ +*.tgz + +# Swift artifacts +.build/ +Package.resolved + +# Go artifacts +go.sum +_obj/ + +# Python artifacts +.venv/ +dist/ +*.egg-info +*.whl + +# C artifacts +*.a +*.so +*.so.* +*.dylib +*.dll +*.pc + +# Example dirs +/examples/*/ + +# Grammar volatiles +*.wasm +*.obj +*.o + +src/ + +.editorconfig +Cargo.toml +Makefile +Package.swift +binding.gyp +bindings/ +go.mod +package.json +pyproject.toml +setup.py + +a.out.js diff --git a/tree-sitter-smithyql/example.smithyql b/tree-sitter-smithyql/example.smithyql new file mode 100644 index 00000000..8124bf33 --- /dev/null +++ b/tree-sitter-smithyql/example.smithyql @@ -0,0 +1,25 @@ +use service a.b#C + +hello { + a = 42, + b = 50, + c = { + d = "foo", + e = false, + f = true, + list = [ + 50, + 100, + 100, + [ + null, + false, + true, + null, + "a", + 40, + ], + ], + }, + nul = null, +} diff --git a/tree-sitter-smithyql/grammar.js b/tree-sitter-smithyql/grammar.js new file mode 100644 index 00000000..01ee9c70 --- /dev/null +++ b/tree-sitter-smithyql/grammar.js @@ -0,0 +1,92 @@ +// Comma-separated sequence of field, with an optional trailing comma. +function comma_separated_trailing(field_grammar) { + return prec.left( + 1, + seq(field_grammar, repeat(seq(",", field_grammar)), optional(",")) + ); +} + +module.exports = grammar({ + name: "smithyql", + + extras: ($) => [$.whitespace, $.comment], + rules: { + source_file: ($) => + seq( + field("prelude", optional($.prelude)), + field("statements", repeat($.top_level_statement)) + ), + + prelude: ($) => repeat1($.use_clause), + + // todo: use token.immediate to prevent comments? + // or just allow comments everywhere? + use_clause: ($) => + seq( + "use", + $.whitespace, + "service", + $.whitespace, + field("identifier", $.qualified_identifier) + ), + + top_level_statement: ($) => choice($.run_query), + + run_query: ($) => + seq( + field("operation_name", $.query_operation_name), + field("input", $.struct) + ), + + _namespace: ($) => seq($.identifier, repeat(seq(".", $.identifier))), + + qualified_identifier: ($) => + seq( + field("namespace", $._namespace), + "#", + field("selection", $.identifier) + ), + + query_operation_name: ($) => + choice( + seq( + field("service_identifier", $.qualified_identifier), + ".", + field("name", $.operation_name) + ), + field("name", $.operation_name) + ), + + operation_name: ($) => $.identifier, + + _input_node: ($) => + choice($.struct, $.list, $.number, $.string, $.boolean, $.null), + + struct: ($) => seq("{", field("bindings", optional($._bindings)), "}"), + list: ($) => seq("[", field("list_fields", optional($._list_fields)), "]"), + + _bindings: ($) => comma_separated_trailing($.binding), + + binding: ($) => + seq( + field("key", $.identifier), + choice("=", ":"), + field("value", $._input_node) + ), + + _list_fields: ($) => comma_separated_trailing($._input_node), + + identifier: ($) => /[a-zA-Z_][a-zA-Z0-9_]*/, + + boolean: ($) => choice("true", "false"), + number: ($) => /-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/, + string: ($) => /"([^"\\]|\\.)*"/, + + null: ($) => "null", + + comment: ($) => token(seq("//", /.*/)), + whitespace: ($) => /\s+/, + }, + supertypes: ($) => [$._input_node], +}); +// diff --git a/tree-sitter-smithyql/test/corpus/simple-complete.txt b/tree-sitter-smithyql/test/corpus/simple-complete.txt new file mode 100644 index 00000000..24a05445 --- /dev/null +++ b/tree-sitter-smithyql/test/corpus/simple-complete.txt @@ -0,0 +1,44 @@ +================================================================================ +Simple complete valid example +================================================================================ +use service foo.bar#baz +Hello { a = 42, b = "false", c = true } +-------------------------------------------------------------------------------- +(source_file + (use_clause + (whitespace) + (whitespace) + (qualified_identifier + (identifier) + (identifier) + (identifier))) + (whitespace) + (top_level_statement + (operation_call + (operation_name + (identifier)) + (whitespace) + (struct + (whitespace) + (bindings + (binding + (identifier) + (whitespace) + (whitespace) + (input_node + (number))) + (whitespace) + (binding + (identifier) + (whitespace) + (whitespace) + (input_node + (string))) + (whitespace) + (binding + (identifier) + (whitespace) + (whitespace) + (input_node + (boolean)))) + (whitespace))))) diff --git a/update-libs.sh b/update-libs.sh new file mode 100755 index 00000000..1fe5c084 --- /dev/null +++ b/update-libs.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +LIBS_PATH=$(nix build .#tree-sitter-smithyql-all --no-link --print-out-paths --print-build-logs) +mkdir -p modules/treesitter/src/main/resources +cp -R "$LIBS_PATH"/* modules/treesitter/src/main/resources +chmod -R 755 modules/treesitter/src/main/resources