+ intro SourceCode, tying together source code text with its *origin*; Prog8Parser now only accepts this

This commit is contained in:
meisl 2021-06-21 12:02:36 +02:00
parent 7b89228fa7
commit af209ad50e
7 changed files with 307 additions and 135 deletions

View File

@ -8,12 +8,12 @@ import prog8.ast.base.Position
import prog8.ast.base.SyntaxError
import prog8.ast.statements.Directive
import prog8.ast.statements.DirectiveArg
import java.io.InputStream
import kotlin.io.FileSystemException
import java.net.URL
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.Path // TODO: use kotlin.io.paths.Path instead
import java.nio.file.Paths // TODO: use kotlin.io.paths.Path instead
fun moduleName(fileName: Path) = fileName.toString().substringBeforeLast('.')
@ -38,7 +38,7 @@ class ModuleImporter(private val program: Program,
else
println("")
val module = Prog8Parser.parseModule(filePath)
val module = Prog8Parser.parseModule(SourceCode.fromPath(filePath))
module.program = program
module.linkParents(program.namespace)
@ -65,7 +65,7 @@ class ModuleImporter(private val program: Program,
private fun importModule(stream: CharStream, modulePath: Path): Module {
val parser = Prog8Parser
val sourceText = stream.toString()
val moduleAst = parser.parseModule(sourceText)
val moduleAst = parser.parseModule(SourceCode.of(sourceText))
moduleAst.program = program
moduleAst.linkParents(program.namespace)
program.modules.add(moduleAst)
@ -92,16 +92,13 @@ class ModuleImporter(private val program: Program,
if(existing!=null)
return null
val rsc = tryGetModuleFromResource("$moduleName.p8", compilationTargetName)
val srcCode = tryGetModuleFromResource("$moduleName.p8", compilationTargetName)
val importedModule =
if(rsc!=null) {
if (srcCode != null) { // found in resources
// load the module from the embedded resource
val (resource, resourcePath) = rsc
resource.use {
println("importing '$moduleName' (library)")
val content = it.reader().readText().replace("\r\n", "\n")
importModule(CharStreams.fromString(content), Module.pathForResource(resourcePath))
}
println("importing '$moduleName' (library): ${srcCode.origin}")
val path = Path.of(URL(srcCode.origin).file)
importModule(srcCode.getCharStream(), path)
} else {
val modulePath = tryGetModuleFromFile(moduleName, source, import.position)
importModule(modulePath)
@ -120,17 +117,16 @@ class ModuleImporter(private val program: Program,
importedModule.statements.addAll(0, directives)
}
private fun tryGetModuleFromResource(name: String, compilationTargetName: String): Pair<InputStream, String>? {
val targetSpecificPath = "/prog8lib/$compilationTargetName/$name"
val targetSpecificResource = object{}.javaClass.getResourceAsStream(targetSpecificPath)
if(targetSpecificResource!=null)
return Pair(targetSpecificResource, targetSpecificPath)
val generalPath = "/prog8lib/$name"
val generalResource = object{}.javaClass.getResourceAsStream(generalPath)
if(generalResource!=null)
return Pair(generalResource, generalPath)
private fun tryGetModuleFromResource(name: String, compilationTargetName: String): SourceCode? {
// try target speficic first
try {
return SourceCode.fromResources("/prog8lib/$compilationTargetName/$name")
} catch (e: FileSystemException) {
}
try {
return SourceCode.fromResources("/prog8lib/$name")
} catch (e: FileSystemException) {
}
return null
}

View File

@ -27,17 +27,9 @@ private fun RecognitionException.getPosition(provenance: String) : Position {
object Prog8Parser {
fun parseModule(srcPath: Path): Module {
return parseModule(CharStreams.fromPath(srcPath), srcPath.fileName.toString())
}
fun parseModule(srcText: String): Module {
return parseModule(CharStreams.fromString(srcText), "<String@${System.identityHashCode(srcText).toString(16)}>")
}
private fun parseModule(chars: CharStream, provenance: String): Module {
val antlrErrorListener = AntlrErrorListener(provenance)
val lexer = Prog8ANTLRLexer(chars)
fun parseModule(src: SourceCode): Module {
val antlrErrorListener = AntlrErrorListener(src.origin)
val lexer = Prog8ANTLRLexer(src.getCharStream())
lexer.removeErrorListeners()
lexer.addErrorListener(antlrErrorListener)
val tokens = CommonTokenStream(lexer)

View File

@ -0,0 +1,118 @@
package prog8.parser
import org.antlr.v4.runtime.CharStream
import org.antlr.v4.runtime.CharStreams
import java.io.File
import java.nio.file.Path
import kotlin.io.path.*
/**
* Encapsulates - and ties together - actual source code (=text)
* and its [origin].
*/
abstract class SourceCode() {
/**
* Where this [SourceCode] instance came from.
* This can be one of the following:
* * a normal string representation of a [java.nio.file.Path], if it originates from a file (see [fromPath])
* * `<String@44c56085>` if was created via [of]
* * `<res:/x/y/z.ext>` if it came from resources (see [fromResources])
*/
abstract val origin: String
abstract fun getCharStream(): CharStream
/**
* The source code as plain string.
* *Note: this is meant for testing and debugging, do NOT use in application code!*
*/
fun asString() = this.getCharStream().toString()
/**
* Deliberately does NOT return the actual text.
* Use [getCharStream].
*/
final override fun toString() = super.toString()
// "static" factory methods
companion object {
/**
* Turn a plain String into a [SourceCode] object.
* [origin] will be something like `<String@44c56085>`.
*/
fun of(text: String): SourceCode {
return object : SourceCode() {
override val origin = "<String@${System.identityHashCode(text).toString(16)}>"
override fun getCharStream(): CharStream {
return CharStreams.fromString(text)
}
}
}
/**
* Get [SourceCode] from the file represented by the specified Path.
* This does not actually *access* the file, but it does check
* whether it
* * exists
* * is a regular file (ie: not a directory)
* * and is actually readable
*
* [origin] will be the given path in absolute and normalized form.
* @throws NoSuchFileException if the file does not exist
* @throws AccessDeniedException if the given path points to a directory or the file is non-readable for some other reason
*/
fun fromPath(path: Path): SourceCode {
if (!path.exists())
throw NoSuchFileException(path.toFile())
if (path.isDirectory())
throw AccessDeniedException(path.toFile(), reason = "Not a file but a directory")
if (!path.isReadable())
throw AccessDeniedException(path.toFile(), reason = "Is not readable")
val normalized = path.normalize()
return object : SourceCode() {
override val origin = normalized.absolutePathString()
override fun getCharStream(): CharStream {
return CharStreams.fromPath(normalized)
}
}
}
/**
* [origin]: `<res:/x/y/z.p8>` for a given `pathString` of "x/y/z.p8"
*/
fun fromResources(pathString: String): SourceCode {
val path = Path.of(pathString).normalize()
val sep = "/"
val normalized = sep + path.toMutableList().joinToString(sep)
val rscURL = object{}.javaClass.getResource(normalized)
if (rscURL == null) {
val rscRoot = object{}.javaClass.getResource("/")
throw NoSuchFileException(
File(normalized),
reason = "looked in resources rooted at $rscRoot")
}
return object : SourceCode() {
override val origin = "<res:$normalized>"
override fun getCharStream(): CharStream {
val inpStr = object{}.javaClass.getResourceAsStream(normalized)
val chars = CharStreams.fromStream(inpStr)
return chars
}
}
}
// TODO: possibly more, like fromURL(..)
/* // For `jar:..` URLs
// see https://stackoverflow.com/questions/22605666/java-access-files-in-jar-causes-java-nio-file-filesystemnotfoundexception
var url = URL("jar:file:/E:/x16/prog8(meisl)/compiler/build/libs/prog8compiler-7.0-BETA3-all.jar!/prog8lib/c64/textio.p8")
val uri = url.toURI()
val parts = uri.toString().split("!")
val fs = FileSystems.newFileSystem(URI.create(parts[0]), mutableMapOf(Pair("", "")) )
val path = fs.getPath(parts[1])
*/
}
}

View File

@ -1,5 +1,11 @@
package prog8tests
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import kotlin.test.*
import java.nio.file.Path // TODO: use kotlin.io.path.Path instead
import kotlin.io.path.*
import prog8.ast.IBuiltinFunctions
import prog8.ast.IMemSizer
import prog8.ast.IStringEncoding
@ -11,11 +17,6 @@ import prog8.ast.expressions.InferredTypes
import prog8.ast.expressions.NumericLiteralValue
import prog8.parser.ModuleImporter
import prog8.parser.ParseError
import java.nio.file.Path
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import kotlin.io.path.*
import kotlin.test.*
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ -51,7 +52,7 @@ class TestModuleImporter {
val srcPath = Path.of("test", "fixtures", "i_do_not_exist")
assertFalse(srcPath.exists(), "sanity check: file should not exist")
assertFailsWith<java.nio.file.NoSuchFileException> { importer.importModule(srcPath) }
assertFailsWith<NoSuchFileException> { importer.importModule(srcPath) }
}
@Test
@ -66,18 +67,7 @@ class TestModuleImporter {
// fn importModule(Path) used to check *.isReadable()*, but NOT .isRegularFile():
assertTrue(srcPath.isReadable(), "sanity check: should still be readable")
assertFailsWith<java.nio.file.AccessDeniedException> { importer.importModule(srcPath) }
}
@Test
fun testImportLibraryModuleWithNonExistingPath() {
val program = Program("foo", mutableListOf(), DummyFunctions, DummyMemsizer)
val importer = ModuleImporter(program, DummyEncoding, "blah", listOf("./test/fixtures"))
val srcPath = Path.of("i_do_not_exist.p8")
assertFalse(srcPath.exists(), "sanity check: file should not exist")
assertFailsWith<java.nio.file.NoSuchFileException> { importer.importLibraryModule(srcPath.nameWithoutExtension) }
assertFailsWith<AccessDeniedException> { importer.importModule(srcPath) }
}
@Test
@ -86,13 +76,14 @@ class TestModuleImporter {
val importer = ModuleImporter(program, DummyEncoding, "blah", listOf("./test/fixtures"))
val filename = "file_with_syntax_error.p8"
val act = { importer.importModule(Path.of("test", "fixtures", filename )) }
val path = Path.of("test", "fixtures", filename)
val act = { importer.importModule(path) }
assertFailsWith<ParseError> { act() }
try {
act()
} catch (e: ParseError) {
assertEquals(filename, e.position.file, "provenance; should be the path's filename, incl. extension '.p8'")
assertEquals(path.absolutePathString(), e.position.file)
assertEquals(2, e.position.line, "line; should be 1-based")
assertEquals(6, e.position.startCol, "startCol; should be 0-based" )
assertEquals(6, e.position.endCol, "endCol; should be 0-based")
@ -114,13 +105,28 @@ class TestModuleImporter {
try {
act()
} catch (e: ParseError) {
assertEquals(imported.fileName.toString(), e.position.file, "provenance; should be the importED file's filename, incl. extension '.p8'")
val expectedProvenance = imported.absolutePathString()
assertEquals(expectedProvenance, e.position.file)
assertEquals(2, e.position.line, "line; should be 1-based")
assertEquals(6, e.position.startCol, "startCol; should be 0-based" )
assertEquals(6, e.position.endCol, "endCol; should be 0-based")
}
}
@Test
fun testImportLibraryModuleWithNonExistingName() {
val program = Program("foo", mutableListOf(), DummyFunctions, DummyMemsizer)
val importer = ModuleImporter(program, DummyEncoding, "blah", listOf("./test/fixtures"))
val filenameNoExt = "i_do_not_exist"
val filenameWithExt = filenameNoExt + ".p8"
val srcPathNoExt = Path.of("test", "fixtures", filenameNoExt)
val srcPathWithExt = Path.of("test", "fixtures", filenameWithExt)
assertFalse(srcPathNoExt.exists(), "sanity check: file should not exist")
assertFalse(srcPathWithExt.exists(), "sanity check: file should not exist")
assertFailsWith<NoSuchFileException> { importer.importLibraryModule(filenameNoExt) }
assertFailsWith<NoSuchFileException> { importer.importLibraryModule(filenameWithExt) }
}
@Test
fun testImportLibraryModuleWithSyntaxError() {
@ -128,17 +134,16 @@ class TestModuleImporter {
val importer = ModuleImporter(program, DummyEncoding, "blah", listOf("./test/fixtures"))
val filename = "file_with_syntax_error"
val act = { importer.importLibraryModule(filename) }
assertFailsWith<ParseError> { act() }
try {
act()
} catch (e: ParseError) {
assertEquals(
filename + ".p8",
e.position.file,
"provenance; should be the path's filename, incl. extension '.p8'"
)
val expectedProvenance = Path.of("test", "fixtures", filename + ".p8")
.absolutePathString()
assertEquals(expectedProvenance, e.position.file)
assertEquals(2, e.position.line, "line; should be 1-based")
assertEquals(6, e.position.startCol, "startCol; should be 0-based")
assertEquals(6, e.position.endCol, "endCol; should be 0-based")
@ -148,7 +153,8 @@ class TestModuleImporter {
@Test
fun testImportLibraryModuleWithImportingBadModule() {
val program = Program("foo", mutableListOf(), DummyFunctions, DummyMemsizer)
val importer = ModuleImporter(program, DummyEncoding, "blah", listOf("./test/fixtures"))
val libdirs = listOf("./test/fixtures")
val importer = ModuleImporter(program, DummyEncoding, "blah", libdirs)
val importing = "import_file_with_syntax_error"
val imported = "file_with_syntax_error"
@ -158,11 +164,10 @@ class TestModuleImporter {
try {
act()
} catch (e: ParseError) {
assertEquals(
imported + ".p8",
e.position.file,
"provenance; should be the importED file's name, incl. extension '.p8'"
)
val expectedProvenance = Path.of(libdirs[0], imported + ".p8")
.normalize()
.absolutePathString()
assertEquals(expectedProvenance, e.position.file)
assertEquals(2, e.position.line, "line; should be 1-based")
assertEquals(6, e.position.startCol, "startCol; should be 0-based")
assertEquals(6, e.position.endCol, "endCol; should be 0-based")

View File

@ -1,26 +1,24 @@
package prog8tests
import org.junit.jupiter.api.Test
import kotlin.test.*
import java.nio.file.Path // TODO: use kotlin.io.path.Path instead
import kotlin.io.path.*
import prog8.ast.statements.Block
import prog8.parser.ParseError
import prog8.parser.Prog8Parser
import prog8.parser.Prog8Parser.parseModule
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import kotlin.io.path.isReadable
import kotlin.io.path.isRegularFile
import kotlin.test.*
import prog8.parser.SourceCode
class TestProg8Parser {
@Test
fun testModuleSourceNeedNotEndWithNewline() {
val nl = "\n" // say, Unix-style (different flavours tested elsewhere)
val srcText = "foo {" + nl + "}" // source ends with '}' (= NO newline, issue #40)
val src = SourceCode.of("foo {" + nl + "}") // source ends with '}' (= NO newline, issue #40)
// #45: Prog8ANTLRParser would report (throw) "missing <EOL> at '<EOF>'"
val module = parseModule(srcText)
val module = parseModule(src)
assertEquals(1, module.statements.size)
}
@ -28,7 +26,7 @@ class TestProg8Parser {
fun testModuleSourceMayEndWithNewline() {
val nl = "\n" // say, Unix-style (different flavours tested elsewhere)
val srcText = "foo {" + nl + "}" + nl // source does end with a newline (issue #40)
val module = parseModule(srcText)
val module = parseModule(SourceCode.of(srcText))
assertEquals(1, module.statements.size)
}
@ -42,8 +40,8 @@ class TestProg8Parser {
// GOOD: 2nd block `bar` does start on a new line; however, a nl at the very end ain't needed
val srcGood = "foo {" + nl + "}" + nl + "bar {" + nl + "}"
assertFailsWith<ParseError> { parseModule(srcBad) }
val module = parseModule(srcGood)
assertFailsWith<ParseError> { parseModule(SourceCode.of(srcBad)) }
val module = parseModule(SourceCode.of(srcGood))
assertEquals(2, module.statements.size)
}
@ -71,7 +69,7 @@ class TestProg8Parser {
"}" +
nlUnix // end with newline (see testModuleSourceNeedNotEndWithNewline)
val module = parseModule(srcText)
val module = parseModule(SourceCode.of(srcText))
assertEquals(2, module.statements.size)
}
@ -86,7 +84,7 @@ class TestProg8Parser {
blockA {
}
"""
val module = parseModule(srcText)
val module = parseModule(SourceCode.of(srcText))
assertEquals(1, module.statements.size)
}
@ -103,7 +101,7 @@ class TestProg8Parser {
blockB {
}
"""
val module = parseModule(srcText)
val module = parseModule(SourceCode.of(srcText))
assertEquals(2, module.statements.size)
}
@ -118,7 +116,7 @@ class TestProg8Parser {
; comment
"""
val module = parseModule(srcText)
val module = parseModule(SourceCode.of(srcText))
assertEquals(1, module.statements.size)
}
@ -127,66 +125,43 @@ class TestProg8Parser {
// issue: #47
// block and block
assertFailsWith<ParseError>{ parseModule("""
assertFailsWith<ParseError>{ parseModule(SourceCode.of("""
blockA {
} blockB {
}
""") }
""")) }
// block and directive
assertFailsWith<ParseError>{ parseModule("""
assertFailsWith<ParseError>{ parseModule(SourceCode.of("""
blockB {
} %import textio
""") }
""")) }
// The following two are bogus due to directive *args* expected to follow the directive name.
// Leaving them in anyways.
// dir and block
assertFailsWith<ParseError>{ parseModule("""
assertFailsWith<ParseError>{ parseModule(SourceCode.of("""
%import textio blockB {
}
""") }
""")) }
assertFailsWith<ParseError>{ parseModule("""
assertFailsWith<ParseError>{ parseModule(SourceCode.of("""
%import textio %import syslib
""") }
""")) }
}
@Test
fun testParseModuleWithDirectoryPath() {
val srcPath = Path.of("test", "fixtures")
assertTrue(srcPath.isDirectory(), "sanity check: should be a directory")
assertFailsWith<java.nio.file.AccessDeniedException> { Prog8Parser.parseModule(srcPath) }
}
fun parseModuleShouldNotLookAtImports() {
val imported = "i_do_not_exist"
val pathNoExt = Path.of(imported).absolute()
val pathWithExt = Path.of("${pathNoExt}.p8")
val text = "%import $imported"
@Test
fun testParseModuleWithNonExistingPath() {
val srcPath = Path.of("test", "fixtures", "i_do_not_exist")
assertFalse(srcPath.exists(), "sanity check: file should not exist")
assertFailsWith<java.nio.file.NoSuchFileException> { Prog8Parser.parseModule(srcPath) }
}
assertFalse(pathNoExt.exists(), "sanity check: file should not exist: $pathNoExt")
assertFalse(pathWithExt.exists(), "sanity check: file should not exist: $pathWithExt")
@Test
fun testParseModuleWithPathMissingExtension_p8() {
val srcPathWithoutExt = Path.of("test", "fixtures", "file_with_syntax_error")
val srcPathWithExt = Path.of(srcPathWithoutExt.toString() + ".p8")
assertTrue(srcPathWithExt.isRegularFile(), "sanity check: should be normal file")
assertTrue(srcPathWithExt.isReadable(), "sanity check: should be readable")
assertFailsWith<java.nio.file.NoSuchFileException> { Prog8Parser.parseModule(srcPathWithoutExt) }
}
@Test
fun testParseModuleWithStringShouldNotLookAtImports() {
val srcText = "%import i_do_not_exist"
val module = Prog8Parser.parseModule(srcText)
assertEquals(1, module.statements.size)
}
@Test
fun testParseModuleWithPathShouldNotLookAtImports() {
val srcPath = Path.of("test", "fixtures", "import_nonexisting.p8")
val module = Prog8Parser.parseModule(srcPath)
val module = parseModule(SourceCode.of(text))
assertEquals(1, module.statements.size)
}
@ -194,9 +169,9 @@ class TestProg8Parser {
fun testErrorLocationForSourceFromString() {
val srcText = "bad * { }\n"
assertFailsWith<ParseError> { parseModule(srcText) }
assertFailsWith<ParseError> { parseModule(SourceCode.of(srcText)) }
try {
parseModule(srcText)
parseModule(SourceCode.of(srcText))
} catch (e: ParseError) {
// Note: assertContains expects *actual* value first
assertContains(e.position.file, Regex("^<String@[0-9a-f]+>$"))
@ -211,11 +186,11 @@ class TestProg8Parser {
val filename = "file_with_syntax_error.p8"
val path = Path.of("test", "fixtures", filename)
assertFailsWith<ParseError> { parseModule(path) }
assertFailsWith<ParseError> { parseModule(SourceCode.fromPath(path)) }
try {
parseModule(path)
parseModule(SourceCode.fromPath(path))
} catch (e: ParseError) {
assertEquals(filename, e.position.file, "provenance; should be the path's filename, incl. extension '.p8'")
assertEquals(path.absolutePathString(), e.position.file, "provenance; should be the path's filename, incl. extension '.p8'")
assertEquals(2, e.position.line, "line; should be 1-based")
assertEquals(6, e.position.startCol, "startCol; should be 0-based" )
assertEquals(6, e.position.endCol, "endCol; should be 0-based")
@ -224,13 +199,13 @@ class TestProg8Parser {
@Test
fun testProg8Ast() {
val module = parseModule("""
main {
sub start() {
return
}
}
""")
val module = parseModule(SourceCode.of("""
main {
sub start() {
return
}
}
"""))
assertIs<Block>(module.statements.first())
assertEquals((module.statements.first() as Block).name, "main")
}

View File

@ -0,0 +1,82 @@
package prog8tests
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import kotlin.test.*
import java.nio.file.Path // TODO: use kotlin.io.path.Path instead
import kotlin.io.path.*
import prog8.parser.SourceCode
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestSourceCode {
@Test
fun testFactoryMethod_Of() {
val text = """
main { }
""".trimIndent()
val src = SourceCode.of(text)
val actualText = src.getCharStream().toString()
assertContains(src.origin, Regex("^<String@[0-9a-f]+>$"))
assertEquals(text, actualText)
}
@Test
fun testFromPathWithNonExistingPath() {
val filename = "i_do_not_exist.p8"
val path = Path.of("test", "fixtures", filename)
assertFalse(path.exists(), "sanity check: file should not exist: ${path.absolute()}")
assertFailsWith<NoSuchFileException> { SourceCode.fromPath(path) }
}
@Test
fun testFromPathWithMissingExtension_p8() {
val pathWithoutExt = Path.of("test", "fixtures", "simple_main")
val pathWithExt = Path.of(pathWithoutExt.toString() + ".p8")
assertTrue(pathWithExt.isRegularFile(), "sanity check: should be normal file: ${pathWithExt.absolute()}")
assertTrue(pathWithExt.isReadable(), "sanity check: should be readable: ${pathWithExt.absolute()}")
assertFailsWith<NoSuchFileException> { SourceCode.fromPath(pathWithoutExt) }
}
@Test
fun testFromPathWithDirectory() {
val path = Path.of("test", "fixtures")
assertTrue(path.isDirectory(), "sanity check: should be a directory")
assertFailsWith<AccessDeniedException> { SourceCode.fromPath(path) }
}
@Test
fun testFromPathWithExistingPath() {
val filename = "simple_main.p8"
val path = Path.of("test", "fixtures", filename)
val src = SourceCode.fromPath(path)
val expectedOrigin = path.normalize().absolutePathString()
assertEquals(expectedOrigin, src.origin)
val expectedSrcText = path.toFile().readText()
val actualSrcText = src.getCharStream().toString()
assertEquals(expectedSrcText, actualSrcText)
}
@Test
fun testFromPathWithExistingNonNormalizedPath() {
val filename = "simple_main.p8"
val path = Path.of(".", "test", "..", "test", "fixtures", filename)
val src = SourceCode.fromPath(path)
val expectedOrigin = path.normalize().absolutePathString()
assertEquals(expectedOrigin, src.origin)
val expectedSrcText = path.toFile().readText()
val actualSrcText = src.getCharStream().toString()
assertEquals(expectedSrcText, actualSrcText)
}
}

View File

@ -0,0 +1,4 @@
main {
sub start() {
}
}