diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f855676 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,42 @@ +# outline-parser - Changelog + +## 2.0.0-beta.1 + +### General + +- **(BREAKING)** Renamed Scala packages from `com.financialforce.*` to `io.github.apexdevtools.*` to align with the Maven group ID `io.github.apex-dev-tools`. + - `com.financialforce.oparser` → `io.github.apexdevtools.oparser` + - `com.financialforce.types` → `io.github.apexdevtools.types` + - `com.financialforce.types.base` → `io.github.apexdevtools.types.base` + - Consumers must update all imports. + +- **(BREAKING)** Merged the `apex-types` library into this artifact. The previously separate `apex-types` dependency is no longer required and should be removed from consuming projects. + - All type interfaces (`io.github.apexdevtools.types.*`), base types (`Modifier`, `Annotation`, `Location`), the `io.github.apexdevtools.api.*` Issue API, and the `io.github.apexdevtools.spi.AnalysisProvider` SPI are now provided directly by `outline-parser`. + +- **(BREAKING)** `Location` semantics changed to match ANTLR conventions: + - `lineOffset` is now 0-based (previously 1-based). + - End positions are now exclusive — locations represent the half-open interval `[start, end)` for both columns and byte offsets. + +- **(BREAKING)** Removed `intern()` methods and static caches on `Annotation` and `Modifier` companion objects (and corresponding `intern()` calls in `Types.scala` factory methods). The caches ignored the `location` field when comparing equality, returning cached instances with wrong locations under parallel use. Consumers relying on `intern()` should construct instances directly. + +- Added optional `location` field to `Modifier` and `Annotation`, populated during parsing: + - Compound modifiers (e.g. `with sharing`) get a spanned location covering both tokens. + - Annotations get a span from `@` through any parameter list. + +- Fixed multi-line string tokenization (Salesforce Summer '26 `'''…'''` literals) to correctly handle multi-byte characters and avoid mis-tokenizing trailing content. + +### Build & dependencies + +- Removed the `apex-ls` test dependency. The JVM comparison test suite now uses `apex-parser` (5.x) directly via `ApexParserFactory`, eliminating the outline-parser ↔ apex-ls cycle. JS-side sample comparison is dropped; the JS test suite is reduced to `SmokeTest`. +- Upgraded Scala.js to `1.18.2`. +- Updated sbt and plugin dependencies to latest stable versions. + +### JS / NPM + +- Bumped peer `@apexdevtools/apex-parser` to `^5.0.0` (was `^4.3.1`). +- Raised minimum Node version to `^20.19.0 || ^22.13.0 || >=24` (was `>=14.0.0`), matching the requirements of `apex-parser 5.x`. +- Moved `@apexdevtools/apex-parser` to `devDependencies`. + +## 1.3.0 + +Last release of the pre-merge line. See git history for prior changes; this changelog begins with the 2.0.0 line. diff --git a/build.sbt b/build.sbt index 4b2d54e..9b5ca68 100644 --- a/build.sbt +++ b/build.sbt @@ -43,13 +43,16 @@ lazy val parser = crossProject(JSPlatform, JVMPlatform) name := "outline-parser", scalacOptions += "-deprecation", libraryDependencies ++= Seq( - "org.scalatest" %%% "scalatest" % "3.2.9" % Test, - "io.github.apex-dev-tools" %%% "apex-ls" % "6.0.2" % Test + "org.scalatest" %%% "scalatest" % "3.2.9" % Test ) ) .jvmSettings( build := buildJVM.value, Test / fork := true, + libraryDependencies ++= Seq( + "io.github.apex-dev-tools" % "apex-parser" % "5.0.0" % Test, + "org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.4" % Test + ), packageOptions += Package.ManifestAttributes( "Class-Path" -> (Compile / dependencyClasspath).value.files.map(_.getName.trim).mkString(" "), "Implementation-Build" -> java.time.Instant.now().toEpochMilli.toString diff --git a/js/npm/package-lock.json b/js/npm/package-lock.json index 3b38dba..3423c20 100644 --- a/js/npm/package-lock.json +++ b/js/npm/package-lock.json @@ -6,134 +6,53 @@ "": { "name": "@apexdevtools/outline-parser", "devDependencies": { - "@apexdevtools/apex-parser": "^4.3.1" + "@apexdevtools/apex-parser": "^5.0.0" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@apexdevtools/apex-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@apexdevtools/apex-parser/-/apex-parser-4.4.1.tgz", - "integrity": "sha512-tLHQ8DkI7/aoL9nOax+Xb3OEXk8IK1mTIpcCBaBJ3kk0Mhy4ik9jfQVAoSxjbWo8aLrjz2E4jnjmSU1iZlEt+Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@apexdevtools/apex-parser/-/apex-parser-5.0.0.tgz", + "integrity": "sha512-C29uPVrcZOsnk/irA2BkL9uerpfWlT0oyedhSooGE6QPeZvGw04XGHKKUdTHfUouwse3/UgWubQ6PN5rtq/a+g==", + "bundleDependencies": [ + "antlr4" + ], "dev": true, "license": "BSD-3-Clause", "dependencies": { - "antlr4ts": "0.5.0-alpha.4", - "node-dir": "^0.1.17" + "antlr4": "4.13.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=20.19.0 || >=22.12.0" } }, - "node_modules/antlr4ts": { - "version": "0.5.0-alpha.4", - "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", - "dev": true - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@apexdevtools/apex-parser/node_modules/antlr4": { + "version": "4.13.2", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/node-dir": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", - "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", - "dev": true, - "dependencies": { - "minimatch": "^3.0.2" - }, + "inBundle": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.10.5" + "node": ">=16" } } }, "dependencies": { "@apexdevtools/apex-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@apexdevtools/apex-parser/-/apex-parser-4.4.1.tgz", - "integrity": "sha512-tLHQ8DkI7/aoL9nOax+Xb3OEXk8IK1mTIpcCBaBJ3kk0Mhy4ik9jfQVAoSxjbWo8aLrjz2E4jnjmSU1iZlEt+Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@apexdevtools/apex-parser/-/apex-parser-5.0.0.tgz", + "integrity": "sha512-C29uPVrcZOsnk/irA2BkL9uerpfWlT0oyedhSooGE6QPeZvGw04XGHKKUdTHfUouwse3/UgWubQ6PN5rtq/a+g==", "dev": true, "requires": { - "antlr4ts": "0.5.0-alpha.4", - "node-dir": "^0.1.17" - } - }, - "antlr4ts": { - "version": "0.5.0-alpha.4", - "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "node-dir": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", - "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", - "dev": true, - "requires": { - "minimatch": "^3.0.2" + "antlr4": "4.13.2" + }, + "dependencies": { + "antlr4": { + "version": "4.13.2", + "bundled": true, + "dev": true + } } } } diff --git a/js/npm/package.json b/js/npm/package.json index 925000e..772158f 100644 --- a/js/npm/package.json +++ b/js/npm/package.json @@ -1,9 +1,9 @@ { "name": "@apexdevtools/outline-parser", "devDependencies": { - "@apexdevtools/apex-parser": "^4.3.1" + "@apexdevtools/apex-parser": "^5.0.0" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } } diff --git a/js/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrOps.scala b/js/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrOps.scala deleted file mode 100644 index bb510d9..0000000 --- a/js/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrOps.scala +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2023 Certinia Inc. All rights reserved. - */ - -package io.github.apexdevtools.oparser.testutil - -import io.github.apexdevtools.types.base.Location -import com.nawforce.pkgforce.path.PathLike -import com.nawforce.runtime.parsers.CodeParser.ParserRuleContext -import com.nawforce.runtime.platform.Path -import io.scalajs.nodejs.process.Process - -import scala.scalajs.js - -/* Additional ANTLR helpers for things not supported by apex-ls */ -object AntlrOps { - type AntlrCollection[T] = js.Array[T] - - def samplesDir(): Option[PathLike] = { - try { - Process - .env("SAMPLES") - .toOption - .filter(_.nonEmpty) - .map(Path(_)) - } catch { - case _: Throwable => None - } - } - - implicit class ContextOps[X <: ParserRuleContext](context: X) { - def location: Location = { - // ANTLR uses [start, end) convention with exclusive end positions - // We don't calculate byte offsets here as that would be too expensive - // The comparison code will ignore byte offsets for ANTLR-generated locations - - // Handle UndefOr[Token] in ScalaJS - context.stop might be undefined - val stopToken = context.stop.toOption.getOrElse(context.start) - - Location( - context.start.line, - context.start.charPositionInLine, - 0, // byte offset not calculated - stopToken.line, - stopToken.charPositionInLine + stopToken.text.length, - 0 // byte offset not calculated - ) - } - } -} diff --git a/shared/src/test/scala/io/github/apexdevtools/oparser/SampleTest.scala b/jvm/src/test/scala/io/github/apexdevtools/oparser/SampleTest.scala similarity index 100% rename from shared/src/test/scala/io/github/apexdevtools/oparser/SampleTest.scala rename to jvm/src/test/scala/io/github/apexdevtools/oparser/SampleTest.scala diff --git a/jvm/src/test/scala/io/github/apexdevtools/oparser/cmds/ApexParserCompare.scala b/jvm/src/test/scala/io/github/apexdevtools/oparser/cmds/ApexParserCompare.scala index fe4eb81..75bf4fa 100644 --- a/jvm/src/test/scala/io/github/apexdevtools/oparser/cmds/ApexParserCompare.scala +++ b/jvm/src/test/scala/io/github/apexdevtools/oparser/cmds/ApexParserCompare.scala @@ -5,14 +5,12 @@ package io.github.apexdevtools.oparser.cmds import io.github.apexdevtools.oparser._ import io.github.apexdevtools.oparser.testutil.{AntlrParser, ClassScanner} -import com.nawforce.pkgforce.path.PathLike -import com.nawforce.runtime.platform.Path +import java.nio.file.{Files, Path, Paths} import java.util.concurrent.atomic.AtomicLong import scala.collection.parallel.CollectionConverters.ImmutableIterableIsParallelizable // Command for comparing outputs with apex-parser on one or many files -// NOTE: This needs to live in 'test' due to apex-ls dependency object ApexParserCompare { def main(args: Array[String]): Unit = { System.exit(ApexParserCompare.run(args)) @@ -46,7 +44,7 @@ object ApexParserCompare { } private def parseSeq(display: Boolean, test: Boolean, onlyANTLR: Boolean, inPath: String): Int = { - val absolutePath = Path(inPath) + val absolutePath = Paths.get(inPath).toAbsolutePath println("SEQUENTIAL " + absolutePath.toString) val start = System.currentTimeMillis() @@ -71,11 +69,11 @@ object ApexParserCompare { } private def parsePar(display: Boolean, test: Boolean, onlyANTLR: Boolean, inPath: String): Int = { - val absolutePath = Path(inPath) + val absolutePath = Paths.get(inPath).toAbsolutePath println("PARALLEL " + absolutePath.toString) val start = System.currentTimeMillis() - val files = ClassScanner.scan(absolutePath) + val files = ClassScanner.scan(absolutePath).toVector val timeForFiles = System.currentTimeMillis() - start val all = files.par.map(p => parseFileWithStatus(display, test, onlyANTLR, p)) @@ -127,7 +125,7 @@ object ApexParserCompare { display: Boolean, test: Boolean, onlyANTLR: Boolean, - file: PathLike + file: Path ): Int = { try { parseFile(display, test, onlyANTLR, file) @@ -140,14 +138,9 @@ object ApexParserCompare { } } - private def parseFile( - display: Boolean, - test: Boolean, - onlyANTLR: Boolean, - file: PathLike - ): Unit = { + private def parseFile(display: Boolean, test: Boolean, onlyANTLR: Boolean, file: Path): Unit = { var start = System.currentTimeMillis() - val contentsBytes = file.readBytes().getOrElse(Array()) + val contentsBytes = Files.readAllBytes(file) addReadFileTime(System.currentTimeMillis() - start) start = System.currentTimeMillis() val contentsString: String = new String(contentsBytes, "utf8") diff --git a/jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrOps.scala b/jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrOps.scala index 19bfbe6..5c5125f 100644 --- a/jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrOps.scala +++ b/jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrOps.scala @@ -5,19 +5,18 @@ package io.github.apexdevtools.oparser.testutil import io.github.apexdevtools.types.base.Location -import com.nawforce.pkgforce.path.PathLike -import com.nawforce.runtime.parsers.CodeParser.ParserRuleContext -import com.nawforce.runtime.platform.Path +import org.antlr.v4.runtime.ParserRuleContext + +import java.nio.file.{Path, Paths} /* Additional ANTLR helpers for things not supported by apex-ls */ object AntlrOps { - type AntlrCollection[T] = java.util.List[T] - def samplesDir(): Option[PathLike] = { + def samplesDir(): Option[Path] = { try { Option(System.getenv("SAMPLES")) .filter(_.nonEmpty) - .map(Path(_)) + .map(Paths.get(_)) } catch { case _: Throwable => None } @@ -31,10 +30,10 @@ object AntlrOps { Location( context.start.getLine, context.start.getCharPositionInLine, - 0, // byte offset not calculated + 0, // byte offset not calculated context.stop.getLine, context.stop.getCharPositionInLine + context.stop.getText.length, - 0 // byte offset not calculated + 0 // byte offset not calculated ) } } diff --git a/shared/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrParser.scala b/jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrParser.scala similarity index 70% rename from shared/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrParser.scala rename to jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrParser.scala index 353954f..8a58b98 100644 --- a/shared/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrParser.scala +++ b/jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/AntlrParser.scala @@ -3,17 +3,17 @@ */ package io.github.apexdevtools.oparser.testutil +import io.github.apexdevtools.apexparser.{ApexErrorListener, ApexParser, ApexParserFactory} import io.github.apexdevtools.oparser._ import io.github.apexdevtools.oparser.testutil.AntlrOps._ import io.github.apexdevtools.types.base._ import io.github.apexdevtools.types.{ITypeDeclaration, base} -import com.nawforce.runtime.parsers.{CodeParser, SourceData} -import com.nawforce.runtime.platform.Path -import io.github.apexdevtools.apexparser.ApexParser -import io.github.apexdevtools.apexparser.ApexParser.ModifierContext +import org.antlr.v4.runtime.{CharStreams, CommonTokenStream} +import org.antlr.v4.runtime.tree.TerminalNode import scala.collection.immutable.ArraySeq import scala.collection.mutable +import scala.jdk.CollectionConverters._ object AntlrParser { @@ -25,17 +25,22 @@ object AntlrParser { ) def parse(path: String, contents: Array[Byte]): Option[ITypeDeclaration] = { - - // We re-use apex-ls CodeParser for JS/JVM portability - // Pass bytes directly to SourceData to ensure byte offsets match the original file - val codeParser = CodeParser(Path(path), SourceData(contents)) - val issuesAndCU = codeParser.parseClass() - issuesAndCU.issues.headOption.map(issue => throw new Exception(issue.asString)) - - val td = issuesAndCU.value.typeDeclaration() - val cd = CodeParser.toScala(td.classDeclaration()) - val id = CodeParser.toScala(td.interfaceDeclaration()) - val ed = CodeParser.toScala(td.enumDeclaration()) + val stream = CharStreams.fromString(new String(contents, "UTF-8")) + val errorListener = new CollectingErrorListener + val lexer = ApexParserFactory.createLexer(stream) + lexer.addErrorListener(errorListener) + val parser = ApexParserFactory.createParser(new CommonTokenStream(lexer)) + parser.addErrorListener(errorListener) + + val cu = parser.compilationUnit() + errorListener.firstError.foreach(msg => throw new Exception(msg)) + + val td = cu.typeDeclaration() + if (td == null) return None + + val cd = Option(td.classDeclaration()) + val id = Option(td.interfaceDeclaration()) + val ed = Option(td.enumDeclaration()) if (cd.nonEmpty) { val ctd = new TestClassTypeDeclaration(path, enclosing = null) val (annotations, modifiers) = splitAnnotationsAndModifiers(td.modifier()) @@ -65,11 +70,11 @@ object AntlrParser { } def toId(ctx: ApexParser.IdContext): LocatableIdToken = { - LocatableIdToken(CodeParser.getText(ctx), ctx.location) + LocatableIdToken(ctx.getText, ctx.location) } def toModifier(ctx: ApexParser.ModifierContext): Modifier = { - val modifier = CodeParser.getText(ctx) + val modifier = ctx.getText val len = modifier.length // Add a space back in, it may not have been a single space but very likely it was if (len >= 7 && modifier.toLowerCase.endsWith("sharing")) { @@ -82,21 +87,16 @@ object AntlrParser { } def antlrAnnotation(ctx: ApexParser.AnnotationContext): Annotation = { - val qName = QualifiedName( - CodeParser.toScala(ctx.qualifiedName().id()).map(id => toId(id)).toArray - ) - val args = CodeParser - .toScala(ctx.elementValue()) - .map(CodeParser.getText) - .orElse(CodeParser.toScala(ctx.elementValuePairs()).map(CodeParser.getText)) - .orElse(if (CodeParser.getText(ctx).endsWith("()")) Some("") else None) + val qName = QualifiedName(ctx.qualifiedName().id().asScala.map(id => toId(id)).toArray) + val args = Option(ctx.elementValue()) + .map(_.getText) + .orElse(Option(ctx.elementValuePairs()).map(_.getText)) + .orElse(if (ctx.getText.endsWith("()")) Some("") else None) Annotation(qName.toString, args, Some(ctx.location)) } def antlrTypeList(ctx: ApexParser.TypeListContext): ArraySeq[TypeRef] = { - CodeParser - .toScala(ctx.typeRef()) - .map(tr => antlrTypeRef(tr)) + ArraySeq.from(ctx.typeRef().asScala.map(tr => antlrTypeRef(tr))) } def antlrTypeArguments(ctx: ApexParser.TypeArgumentsContext): ArraySeq[TypeRef] = { @@ -105,22 +105,18 @@ object AntlrParser { def antlrTypeName(ctx: ApexParser.TypeNameContext): TypeNameSegment = { val typeArguments = - CodeParser - .toScala(ctx.typeArguments()) + Option(ctx.typeArguments()) .map(ta => antlrTypeArguments(ta)) .getOrElse(TypeRef.emptyArraySeq) - CodeParser - .toScala(ctx.LIST()) + Option(ctx.LIST(): TerminalNode) .map(l => new TypeNameSegment(LocatableIdToken(l.toString, Location.default), typeArguments)) .getOrElse( - CodeParser - .toScala(ctx.SET()) + Option(ctx.SET(): TerminalNode) .map(l => new TypeNameSegment(LocatableIdToken(l.toString, Location.default), typeArguments) ) .getOrElse( - CodeParser - .toScala(ctx.MAP()) + Option(ctx.MAP(): TerminalNode) .map(l => new TypeNameSegment(LocatableIdToken(l.toString, Location.default), typeArguments) ) @@ -131,15 +127,16 @@ object AntlrParser { def antlrTypeRef(ctx: ApexParser.TypeRefContext): UnresolvedTypeRef = { val segments = new mutable.ArrayBuffer[TypeNameSegment]() - CodeParser - .toScala(ctx.typeName()) + ctx + .typeName() + .asScala .foreach(tn => { segments.append(antlrTypeName(tn)) }) base.UnresolvedTypeRef( segments.toArray, - CodeParser.getText(ctx.arraySubscripts()).count(_ == ']') + Option(ctx.arraySubscripts()).map(_.getText).getOrElse("").count(_ == ']') ) } @@ -149,21 +146,19 @@ object AntlrParser { ): Unit = { ctd.setId(toId(ctx.id())) - CodeParser.toScala(ctx.typeRef()).foreach(tr => ctd.setExtends(antlrTypeRef(tr))) - CodeParser.toScala(ctx.typeList()).foreach(tl => ctd.setImplements(antlrTypeList(tl))) + Option(ctx.typeRef()).foreach(tr => ctd.setExtends(antlrTypeRef(tr))) + Option(ctx.typeList()).foreach(tl => ctd.setImplements(antlrTypeList(tl))) - CodeParser.toScala(ctx.classBody().classBodyDeclaration()).foreach { c => + ctx.classBody().classBodyDeclaration().asScala.foreach { c => { - CodeParser - .toScala(c.memberDeclaration()) + Option(c.memberDeclaration()) .foreach(d => { val md = new MemberDeclaration val (annotations, modifiers) = splitAnnotationsAndModifiers(c.modifier()) md.setAnnotations(annotations) md.setModifiers(modifiers) - CodeParser - .toScala(d.classDeclaration()) + Option(d.classDeclaration()) .foreach(icd => { val innerClassDeclaration = new TestClassTypeDeclaration(ctd.path, ctd) innerClassDeclaration.setAnnotations(md.annotations) @@ -172,8 +167,7 @@ object AntlrParser { antlrClassTypeDeclaration(innerClassDeclaration, icd) }) - CodeParser - .toScala(d.interfaceDeclaration()) + Option(d.interfaceDeclaration()) .foreach(iid => { val innerInterfaceDeclaration = new TestInterfaceTypeDeclaration(ctd.path, ctd) innerInterfaceDeclaration.setAnnotations(md.annotations) @@ -182,8 +176,7 @@ object AntlrParser { antlrInterfaceTypeDeclaration(innerInterfaceDeclaration, iid) }) - CodeParser - .toScala(d.enumDeclaration()) + Option(d.enumDeclaration()) .foreach(ied => { val innerEnumDeclaration = new TestEnumTypeDeclaration(ctd.path, ctd) innerEnumDeclaration.setAnnotations(md.annotations) @@ -192,21 +185,16 @@ object AntlrParser { antlrEnumTypeDeclaration(innerEnumDeclaration, ied) }) - CodeParser - .toScala(d.constructorDeclaration()) + Option(d.constructorDeclaration()) .foreach(antlrConstructorDeclaration(ctd, md, _)) - CodeParser - .toScala(d.methodDeclaration()) + Option(d.methodDeclaration()) .foreach(antlrMethodDeclaration(ctd, md, _)) - CodeParser - .toScala(d.propertyDeclaration()) + Option(d.propertyDeclaration()) .foreach(antlrPropertyDeclaration(ctd, md, _)) - CodeParser - .toScala(d.fieldDeclaration()) + Option(d.fieldDeclaration()) .foreach(antlrFieldDeclaration(ctd, md, _)) }) - CodeParser - .toScala(c.block()) + Option(c.block()) .foreach(_ => { ctd.appendInitializer(Initializer(Option(c.STATIC()).isDefined)) }) @@ -220,14 +208,12 @@ object AntlrParser { ): Unit = { itd.setId(toId(ctx.id())) - CodeParser.toScala(ctx.typeList()).foreach(tl => itd.setImplements(antlrTypeList(tl))) + Option(ctx.typeList()).foreach(tl => itd.setImplements(antlrTypeList(tl))) - CodeParser - .toScala( - ctx - .interfaceBody() - .interfaceMethodDeclaration() - ) + ctx + .interfaceBody() + .interfaceMethodDeclaration() + .asScala .foreach(mctx => { val md = new MemberDeclaration val (annotations, modifiers) = splitAnnotationsAndModifiers(mctx.modifier()) @@ -243,13 +229,9 @@ object AntlrParser { ): Unit = { etd.setId(toId(ctx.id())) - CodeParser - .toScala( - ctx - .enumConstants() - ) - .map(c => CodeParser.toScala(c.id())) - .getOrElse(ArraySeq()) + Option(ctx.enumConstants()) + .map(c => c.id().asScala.toSeq) + .getOrElse(Seq.empty) .foreach(ictx => { val id = toId(ictx) etd.appendField(FieldDeclaration(Array(), Array(Modifier("static")), etd, id)) @@ -262,21 +244,19 @@ object AntlrParser { ctx: ApexParser.ConstructorDeclarationContext ): Unit = { - val qName = QualifiedName(CodeParser.toScala(ctx.qualifiedName().id()).map(toId).toArray) + val qName = + QualifiedName(ctx.qualifiedName().id().asScala.map(toId).toArray) val formalParameterList = - CodeParser - .toScala( - ctx - .formalParameters() - .formalParameterList() - ) + Option( + ctx + .formalParameters() + .formalParameterList() + ) .map(fpl => ArraySeq.unsafeWrapArray( - CodeParser - .toScala( - fpl - .formalParameter() - ) + fpl + .formalParameter() + .asScala .map(antlrFormalParameter) .toArray ) @@ -298,22 +278,19 @@ object AntlrParser { val id = toId(ctx.id()) val formalParameterList = - CodeParser - .toScala(ctx.formalParameters().formalParameterList()) + Option(ctx.formalParameters().formalParameterList()) .map(fpl => ArraySeq.unsafeWrapArray( - CodeParser - .toScala( - fpl - .formalParameter() - ) + fpl + .formalParameter() + .asScala .map(antlrFormalParameter) .toArray ) ) .getOrElse(FormalParameter.emptyArraySeq) - CodeParser.toScala(ctx.typeRef()) match { + Option(ctx.typeRef()) match { case Some(tr) => md.add(antlrTypeRef(tr)) case None => md.typeRef = voidTypeRef } @@ -333,22 +310,19 @@ object AntlrParser { val id = toId(ctx.id()) val formalParameterList = - CodeParser - .toScala(ctx.formalParameters().formalParameterList()) + Option(ctx.formalParameters().formalParameterList()) .map(fpl => ArraySeq.unsafeWrapArray( - CodeParser - .toScala( - fpl - .formalParameter() - ) + fpl + .formalParameter() + .asScala .map(antlrFormalParameter) .toArray ) ) .getOrElse(FormalParameter.emptyArraySeq) - CodeParser.toScala(ctx.typeRef()) match { + Option(ctx.typeRef()) match { case Some(tr) => md.add(antlrTypeRef(tr)) case None => md.typeRef = voidTypeRef } @@ -384,8 +358,10 @@ object AntlrParser { ): Unit = { md.add(antlrTypeRef(ctx.typeRef())) - CodeParser - .toScala(ctx.variableDeclarators().variableDeclarator()) + ctx + .variableDeclarators() + .variableDeclarator() + .asScala .foreach(v => { val id = toId(v.id()) val field = @@ -395,18 +371,26 @@ object AntlrParser { } private def splitAnnotationsAndModifiers( - context: AntlrCollection[ApexParser.ModifierContext] + context: java.util.List[ApexParser.ModifierContext] ): (Array[Annotation], Array[Modifier]) = { - val ctxArray: ArraySeq[ModifierContext] = CodeParser.toScala(context) + val ctxArray = context.asScala ( ctxArray - .flatMap(m => CodeParser.toScala(m.annotation())) + .flatMap(m => Option(m.annotation())) .map(m => antlrAnnotation(m)) .toArray, ctxArray - .filter(m => CodeParser.toScala(m.annotation()).isEmpty) + .filter(m => Option(m.annotation()).isEmpty) .map(toModifier) .toArray ) } + + private class CollectingErrorListener extends ApexErrorListener { + private val errors = mutable.ArrayBuffer.empty[String] + override def apexSyntaxError(line: Int, column: Int, msg: String): Unit = { + errors += s"line $line:$column $msg" + } + def firstError: Option[String] = errors.headOption + } } diff --git a/jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/ClassScanner.scala b/jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/ClassScanner.scala new file mode 100644 index 0000000..7c752fb --- /dev/null +++ b/jvm/src/test/scala/io/github/apexdevtools/oparser/testutil/ClassScanner.scala @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Certinia Inc. All rights reserved. + */ +package io.github.apexdevtools.oparser.testutil + +import java.nio.file.{Files, Path} +import scala.jdk.CollectionConverters._ + +object ClassScanner { + + def load(path: Path): Map[String, Array[Byte]] = { + scan(path).map(file => file.toString -> Files.readAllBytes(file)).toMap + } + + def scan(path: Path): Seq[Path] = { + if (Files.isDirectory(path)) { + val stream = Files.walk(path) + try { + stream + .iterator() + .asScala + .filter(p => Files.isRegularFile(p) && p.toString.endsWith(".cls")) + .toVector + } finally stream.close() + } else { + Seq(path) + } + } +} diff --git a/shared/src/test/scala/io/github/apexdevtools/oparser/BufferTest.scala b/shared/src/test/scala/io/github/apexdevtools/oparser/BufferTest.scala index fc3b7d9..6001a58 100644 --- a/shared/src/test/scala/io/github/apexdevtools/oparser/BufferTest.scala +++ b/shared/src/test/scala/io/github/apexdevtools/oparser/BufferTest.scala @@ -38,7 +38,13 @@ class BufferTest extends AnyFunSpec { buffer.append(byteOffset = 0, charOffset = 0, line = 0, lineOffset = 0, charByteLength = 1) buffer.append(byteOffset = 1, charOffset = 1, line = 0, lineOffset = 1, charByteLength = 1) buffer.append(byteOffset = 2, charOffset = 2, line = 0, lineOffset = 2, charByteLength = 1) - buffer.append(byteOffset = 3, charOffset = 3, line = 0, lineOffset = 3, charByteLength = 2) // é is 2 bytes + buffer.append( + byteOffset = 3, + charOffset = 3, + line = 0, + lineOffset = 3, + charByteLength = 2 + ) // é is 2 bytes val (captured, location) = buffer.captured() @@ -68,8 +74,21 @@ class BufferTest extends AnyFunSpec { // A=1 byte, 🤦=4 bytes (spans 2 char positions), B=1 byte buffer.append(byteOffset = 0, charOffset = 0, line = 0, lineOffset = 0, charByteLength = 1) - buffer.append(byteOffset = 1, charOffset = 1, line = 0, lineOffset = 1, charByteLength = 4, charCount = 2) // emoji - buffer.append(byteOffset = 5, charOffset = 3, line = 0, lineOffset = 2, charByteLength = 1) // B at char position 3 + buffer.append( + byteOffset = 1, + charOffset = 1, + line = 0, + lineOffset = 1, + charByteLength = 4, + charCount = 2 + ) // emoji + buffer.append( + byteOffset = 5, + charOffset = 3, + line = 0, + lineOffset = 2, + charByteLength = 1 + ) // B at char position 3 val (captured, location) = buffer.captured() @@ -83,10 +102,29 @@ class BufferTest extends AnyFunSpec { val buffer = new Buffer(content) // In Java Strings (UTF-16), emoji is 2 chars: high surrogate (index 2) + low surrogate (index 3) - buffer.append(byteOffset = 0, charOffset = 0, line = 0, lineOffset = 0, charByteLength = 1) // 'H' - buffer.append(byteOffset = 1, charOffset = 1, line = 0, lineOffset = 1, charByteLength = 1) // 'i' + buffer.append( + byteOffset = 0, + charOffset = 0, + line = 0, + lineOffset = 0, + charByteLength = 1 + ) // 'H' + buffer.append( + byteOffset = 1, + charOffset = 1, + line = 0, + lineOffset = 1, + charByteLength = 1 + ) // 'i' // Emoji: starts at charOffset 2, spans 2 char positions (surrogate pair), 4 bytes in UTF-8 - buffer.append(byteOffset = 2, charOffset = 2, line = 0, lineOffset = 2, charByteLength = 4, charCount = 2) + buffer.append( + byteOffset = 2, + charOffset = 2, + line = 0, + lineOffset = 2, + charByteLength = 4, + charCount = 2 + ) val (captured, location) = buffer.captured() @@ -119,10 +157,10 @@ class BufferTest extends AnyFunSpec { it("should verify byte offsets match actual UTF-8 encoding") { // This test verifies our byte offset calculation matches actual UTF-8 encoding val testCases = Seq( - ("A", 1), // ASCII - ("é", 2), // 2-byte UTF-8 - ("中", 3), // 3-byte UTF-8 - ("🤦", 4) // 4-byte UTF-8 (surrogate pair) + ("A", 1), // ASCII + ("é", 2), // 2-byte UTF-8 + ("中", 3), // 3-byte UTF-8 + ("🤦", 4) // 4-byte UTF-8 (surrogate pair) ) testCases.foreach { case (char, expectedBytes) => @@ -134,4 +172,4 @@ class BufferTest extends AnyFunSpec { } } } -} \ No newline at end of file +} diff --git a/shared/src/test/scala/io/github/apexdevtools/oparser/Compare.scala b/shared/src/test/scala/io/github/apexdevtools/oparser/Compare.scala index df3d8dd..d7fa93c 100644 --- a/shared/src/test/scala/io/github/apexdevtools/oparser/Compare.scala +++ b/shared/src/test/scala/io/github/apexdevtools/oparser/Compare.scala @@ -17,7 +17,7 @@ object Compare { l1.endLine == l2.endLine && l1.endLineOffset == l2.endLineOffset case (None, None) => true - case _ => false + case _ => false } } diff --git a/shared/src/test/scala/io/github/apexdevtools/oparser/testutil/ClassScanner.scala b/shared/src/test/scala/io/github/apexdevtools/oparser/testutil/ClassScanner.scala deleted file mode 100644 index 3de1995..0000000 --- a/shared/src/test/scala/io/github/apexdevtools/oparser/testutil/ClassScanner.scala +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 Certinia Inc. All rights reserved. - */ -package io.github.apexdevtools.oparser.testutil - -import com.nawforce.pkgforce.path.PathLike - -object ClassScanner { - - def load(path: PathLike): Map[String, Array[Byte]] = { - scan(path) - .flatMap(file => { - file.readBytes().map(c => (file.toString, c)).toOption - }) - .toMap - } - - def scan(path: PathLike): Seq[PathLike] = { - if (path.isDirectory) { - scanDirectory(path) - .filterNot(_.isDirectory) - .filter(_.toString.endsWith(".cls")) - } else { - Seq(path) - } - } - - private def scanDirectory(path: PathLike): Seq[PathLike] = { - path - .directoryList() - .map(names => { - names.flatMap(name => { - val entry = path.join(name) - entry +: (if (entry.isDirectory) scanDirectory(entry) else Nil) - }) - }) - .getOrElse(Nil) - } -}