diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt index fc5ee618f75..28f6cdcf9ee 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/js/dce/K2JSDce.kt @@ -24,28 +24,45 @@ import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.config.Services import org.jetbrains.kotlin.js.dce.* import java.io.File +import java.util.zip.ZipFile class K2JSDce : CLITool() { override fun createArguments(): K2JSDceArguments = K2JSDceArguments() override fun execImpl(messageCollector: MessageCollector, services: Services, arguments: K2JSDceArguments): ExitCode { val baseDir = File(arguments.outputDirectory ?: "min") - val files = arguments.freeArgs.map { arg -> - val parts = arg.split(File.pathSeparator, ignoreCase = false, limit = 2) - val inputName = parts[0] - val moduleName = parts.getOrNull(1) ?: "" - val resolvedModuleName = if (!moduleName.isEmpty()) moduleName else File(inputName).nameWithoutExtension - val pathToSourceMapCandidate = "$inputName.map" - val pathToSourceMap = if (File(pathToSourceMapCandidate).exists()) pathToSourceMapCandidate else null - InputFile(inputName, pathToSourceMap, File(baseDir, resolvedModuleName + ".js").absolutePath, resolvedModuleName) + var hasErrors = false + val files = arguments.freeArgs.flatMap { arg -> + val files = collectInputFiles(baseDir, arg, messageCollector) + if (files != null) { + files + } + else { + hasErrors = true + emptyList() + } } + if (hasErrors) return ExitCode.COMPILATION_ERROR + if (files.isEmpty() && !arguments.version) { messageCollector.report(CompilerMessageSeverity.ERROR, "no source files") return ExitCode.COMPILATION_ERROR } - if (!checkSourceFiles(messageCollector, files)) { - return ExitCode.COMPILATION_ERROR + + val existingFiles = mutableMapOf() + for (file in files) { + existingFiles[file.outputPath]?.let { + messageCollector.report( + CompilerMessageSeverity.ERROR, + "duplicate target file will be created for '${file.resource.name}' and '${it.resource.name}'") + return ExitCode.COMPILATION_ERROR + } + existingFiles[file.outputPath] = file + if (File(file.outputPath).isDirectory) { + messageCollector.report(CompilerMessageSeverity.ERROR, "cannot open output file '${file.outputPath}': it is a directory") + return ExitCode.COMPILATION_ERROR + } } val includedDeclarations = arguments.declarationsToKeep.orEmpty().toSet() @@ -72,35 +89,83 @@ class K2JSDce : CLITool() { return ExitCode.OK } - private fun checkSourceFiles(messageCollector: MessageCollector, files: List): Boolean { - return files.fold(true) { ok, file -> - val inputFile = File(file.path) - val outputFile = File(file.outputPath) - - val inputOk = when { - !inputFile.exists() -> { - messageCollector.report(CompilerMessageSeverity.ERROR, "source file or directory not found: " + file.path) - false - } - inputFile.isDirectory -> { - messageCollector.report(CompilerMessageSeverity.ERROR, "input file '" + file.path + "' is a directory") - false - } - else -> true + private fun collectInputFiles(baseDir: File, fileName: String, messageCollector: MessageCollector): List? { + val file = File(fileName) + return when { + file.isDirectory -> { + collectInputFilesFromDirectory(baseDir, fileName) } - - val outputOk = when { - outputFile.exists() && outputFile.isDirectory -> { - messageCollector.report(CompilerMessageSeverity.ERROR, "cannot open output file '${outputFile.path}': is a directory") - false + file.isFile -> { + when { + fileName.endsWith(".js") -> { + listOf(singleInputFile(baseDir, fileName)) + } + fileName.endsWith(".zip") || fileName.endsWith(".jar") -> { + collectInputFilesFromZip(baseDir, fileName) + } + else -> { + messageCollector.report(CompilerMessageSeverity.ERROR, + "invalid file name '$fileName'; must end either with '.js', '.zip' or '.jar'") + null + } } - else -> true } - - ok and inputOk and outputOk + else -> { + messageCollector.report(CompilerMessageSeverity.ERROR, "source file or directory not found: $fileName") + null + } } } + private fun singleInputFile(baseDir: File, path: String): InputFile { + val moduleName = getModuleNameFromPath(path) + val pathToSourceMapCandidate = "$path.map" + val pathToSourceMap = if (File(pathToSourceMapCandidate).exists()) pathToSourceMapCandidate else null + return InputFile(InputResource.file(path), pathToSourceMap?.let { InputResource.file(it) }, + File(baseDir, "$moduleName.js").absolutePath, moduleName) + } + + private fun collectInputFilesFromZip(baseDir: File, path: String): List { + return ZipFile(path).use { zipFile -> + zipFile.entries().asSequence() + .filter { !it.isDirectory } + .filter { it.name.endsWith(".js") } + .filter { zipFile.getEntry(it.name.metaJs()) != null } + .distinctBy { it.name } + .map { entry -> + val moduleName = getModuleNameFromPath(entry.name) + val pathToSourceMapCandidate = "${entry.name}.map" + val pathToSourceMap = if (zipFile.getEntry(pathToSourceMapCandidate) != null) pathToSourceMapCandidate else null + InputFile(InputResource.zipFile(path, entry.name), pathToSourceMap?.let { InputResource.zipFile(path, it) }, + File(baseDir, "$moduleName.js").absolutePath, moduleName) + } + .toList() + } + } + + private fun collectInputFilesFromDirectory(baseDir: File, path: String): List { + return File(path).walkTopDown().asSequence() + .filter { !it.isDirectory } + .filter { it.name.endsWith(".js") } + .filter { File(it.path.metaJs()).exists() } + .map { entry -> + val moduleName = getModuleNameFromPath(entry.name) + val pathToSourceMapCandidate = "${entry.path}.map" + val pathToSourceMap = if (File(pathToSourceMapCandidate).exists()) pathToSourceMapCandidate else null + InputFile(InputResource.file(entry.path), pathToSourceMap?.let { InputResource.file(it) }, + File(baseDir, "$moduleName.js").absolutePath, moduleName) + } + .toList() + } + + private fun String.metaJs() = removeSuffix(".js") + ".meta.js" + + private fun getModuleNameFromPath(path: String): String { + val dotIndex = path.lastIndexOf('.') + val slashIndex = maxOf(path.lastIndexOf('/'), path.lastIndexOf('\\')) + return path.substring(slashIndex + 1, if (dotIndex < 0) path.length else dotIndex) + } + override fun executableScriptFileName(): String = "kotlin-dce-js" companion object { diff --git a/compiler/testData/cli/js-dce/notFile.out b/compiler/testData/cli/js-dce/notFile.out index 7657d6798fc..997aed27463 100644 --- a/compiler/testData/cli/js-dce/notFile.out +++ b/compiler/testData/cli/js-dce/notFile.out @@ -1,2 +1,2 @@ -error: input file 'compiler/testData/cli/js-dce' is a directory +error: no source files COMPILATION_ERROR diff --git a/compiler/testData/cli/js-dce/outputIsDirectory.out b/compiler/testData/cli/js-dce/outputIsDirectory.out index 2ca5ba23ff5..75cd8219597 100644 --- a/compiler/testData/cli/js-dce/outputIsDirectory.out +++ b/compiler/testData/cli/js-dce/outputIsDirectory.out @@ -1,2 +1,2 @@ -error: cannot open output file '$TESTDATA_DIR$/min/simple.js': is a directory +error: cannot open output file '$TESTDATA_DIR$/min/simple.js': it is a directory COMPILATION_ERROR diff --git a/compiler/testData/cli/js-dce/overrideOutputName.args b/compiler/testData/cli/js-dce/overrideOutputName.args deleted file mode 100644 index 70eaab3b498..00000000000 --- a/compiler/testData/cli/js-dce/overrideOutputName.args +++ /dev/null @@ -1,3 +0,0 @@ -$TESTDATA_DIR$/simple.js:bar --output-dir -$TEMP_DIR$/min \ No newline at end of file diff --git a/compiler/testData/cli/js-dce/overrideOutputName.out b/compiler/testData/cli/js-dce/overrideOutputName.out deleted file mode 100644 index d86bac9de59..00000000000 --- a/compiler/testData/cli/js-dce/overrideOutputName.out +++ /dev/null @@ -1 +0,0 @@ -OK diff --git a/compiler/testData/cli/js-dce/overrideOutputName.test b/compiler/testData/cli/js-dce/overrideOutputName.test deleted file mode 100644 index 1223b61909c..00000000000 --- a/compiler/testData/cli/js-dce/overrideOutputName.test +++ /dev/null @@ -1 +0,0 @@ -// EXISTS: min/bar.js \ No newline at end of file diff --git a/compiler/tests/org/jetbrains/kotlin/cli/CliTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/cli/CliTestGenerated.java index 3d6f05ed9ad..5c9c07becdd 100644 --- a/compiler/tests/org/jetbrains/kotlin/cli/CliTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/cli/CliTestGenerated.java @@ -800,12 +800,6 @@ public class CliTestGenerated extends AbstractCliTest { doJsDceTest(fileName); } - @TestMetadata("overrideOutputName.args") - public void testOverrideOutputName() throws Exception { - String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/overrideOutputName.args"); - doJsDceTest(fileName); - } - @TestMetadata("parseError.args") public void testParseError() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/js-dce/parseError.args"); diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt index 67c20f732db..7fbe068073b 100644 --- a/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/DeadCodeElimination.kt @@ -19,7 +19,10 @@ package org.jetbrains.kotlin.js.dce import com.google.gwt.dev.js.rhino.CodePosition import com.google.gwt.dev.js.rhino.ErrorReporter import org.jetbrains.kotlin.js.backend.JsToStringGenerationVisitor -import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.js.backend.ast.JsBlock +import org.jetbrains.kotlin.js.backend.ast.JsGlobalBlock +import org.jetbrains.kotlin.js.backend.ast.JsNode +import org.jetbrains.kotlin.js.backend.ast.JsProgram import org.jetbrains.kotlin.js.dce.Context.Node import org.jetbrains.kotlin.js.facade.SourceMapBuilderConsumer import org.jetbrains.kotlin.js.inline.util.collectDefinedNames @@ -33,7 +36,6 @@ import org.jetbrains.kotlin.js.sourceMap.SourceFilePathResolver import org.jetbrains.kotlin.js.sourceMap.SourceMap3Builder import org.jetbrains.kotlin.js.util.TextOutputImpl import java.io.File -import java.io.FileInputStream import java.io.InputStreamReader class DeadCodeElimination(private val logConsumer: (DCELogLevel, String) -> Unit) { @@ -41,7 +43,6 @@ class DeadCodeElimination(private val logConsumer: (DCELogLevel, String) -> Unit private val reachableNames = mutableSetOf() var reachableNodes = setOf() - get private set fun apply(root: JsNode) { @@ -82,19 +83,19 @@ class DeadCodeElimination(private val logConsumer: (DCELogLevel, String) -> Unit var hasErrors = false val blocks = inputFiles.map { file -> val block = JsGlobalBlock() - val code = File(file.path).readText() - val statements = parse(code, Reporter(file.path, logConsumer), program.scope, file.path) ?: run { + val code = file.resource.reader().let { InputStreamReader(it, "UTF-8") }.use { it.readText() } + val statements = parse(code, Reporter(file.resource.name, logConsumer), program.scope, file.resource.name) ?: run { hasErrors = true return@map block } - val sourceMapParse = file.pathToSourceMap - ?.let { InputStreamReader(FileInputStream(it), "UTF-8") } + val sourceMapParse = file.sourceMapResource + ?.let { InputStreamReader(it.reader(), "UTF-8") } ?.use { SourceMapParser.parse(it) } when (sourceMapParse) { is SourceMapError -> { logConsumer( DCELogLevel.WARN, - "Error parsing source map file ${file.pathToSourceMap}: ${sourceMapParse.message}") + "Error parsing source map file ${file.sourceMapResource}: ${sourceMapParse.message}") } is SourceMapSuccess -> { val sourceMap = sourceMapParse.value @@ -130,7 +131,7 @@ class DeadCodeElimination(private val logConsumer: (DCELogLevel, String) -> Unit writeText(textOutput.toString()) } - if (file.pathToSourceMap != null) { + if (file.sourceMapResource != null) { sourceMapFile.writeText(sourceMapContent) } } diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt index a0ed91f9c8e..b7839473958 100644 --- a/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputFile.kt @@ -16,4 +16,4 @@ package org.jetbrains.kotlin.js.dce -class InputFile(val path: String, val pathToSourceMap: String?, val outputPath: String, val moduleName: String? = null) \ No newline at end of file +class InputFile(val resource: InputResource, val sourceMapResource: InputResource?, val outputPath: String, val moduleName: String? = null) \ No newline at end of file diff --git a/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputResource.kt b/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputResource.kt new file mode 100644 index 00000000000..f84d84c7190 --- /dev/null +++ b/js/js.dce/src/org/jetbrains/kotlin/js/dce/InputResource.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.js.dce + +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.util.zip.ZipFile + +class InputResource(val name: String, val reader: () -> InputStream) { + companion object { + fun file(path: String): InputResource = InputResource(path) { FileInputStream(File(path)) } + + fun zipFile(path: String, entryPath: String): InputResource = InputResource("$path!$entryPath") { + val zipFile = ZipFile(path) + zipFile.getInputStream(zipFile.getEntry(entryPath)) + } + } +} \ No newline at end of file diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractDceTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractDceTest.kt index 1b37b713238..a0f9321e00e 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractDceTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/AbstractDceTest.kt @@ -19,13 +19,15 @@ package org.jetbrains.kotlin.js.test import junit.framework.TestCase import org.jetbrains.kotlin.js.dce.DeadCodeElimination import org.jetbrains.kotlin.js.dce.InputFile +import org.jetbrains.kotlin.js.dce.InputResource import java.io.File abstract class AbstractDceTest : TestCase() { fun doTest(filePath: String) { val file = File(filePath) val fileContents = file.readText() - val inputFile = InputFile(filePath, null, File(pathToOutputDir, file.relativeTo(File(pathToTestDir)).path).path, "main") + val inputFile = InputFile(InputResource.file(filePath), null, + File(pathToOutputDir, file.relativeTo(File(pathToTestDir)).path).path, "main") val dceResult = DeadCodeElimination.run(setOf(inputFile), extractDeclarations(REQUEST_REACHABLE_PATTERN, fileContents)) { _, _ -> } val reachableNodeStrings = dceResult.reachableNodes.map { it.toString().removePrefix(".") }.toSet() diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt index bbc13f3462e..909410373c5 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt @@ -43,6 +43,7 @@ import org.jetbrains.kotlin.js.config.JsConfig import org.jetbrains.kotlin.js.config.SourceMapSourceEmbedding import org.jetbrains.kotlin.js.dce.DeadCodeElimination import org.jetbrains.kotlin.js.dce.InputFile +import org.jetbrains.kotlin.js.dce.InputResource import org.jetbrains.kotlin.js.facade.* import org.jetbrains.kotlin.js.parser.parse import org.jetbrains.kotlin.js.parser.sourcemaps.SourceMapError @@ -549,12 +550,12 @@ abstract class BasicBoxTest( val kotlinJsLibOutput = File(workDir, "kotlin.min.js").path val kotlinTestJsLibOutput = File(workDir, "kotlin-test.min.js").path - val kotlinJsInputFile = InputFile(kotlinJsLib, null, kotlinJsLibOutput, "kotlin") - val kotlinTestJsInputFile = InputFile(kotlinTestJsLib, null, kotlinTestJsLibOutput, "kotlin-test") + val kotlinJsInputFile = InputFile(InputResource.file(kotlinJsLib), null, kotlinJsLibOutput, "kotlin") + val kotlinTestJsInputFile = InputFile(InputResource.file(kotlinTestJsLib), null, kotlinTestJsLibOutput, "kotlin-test") val filesToMinify = generatedJsFiles.associate { (fileName, module) -> val inputFileName = File(fileName).nameWithoutExtension - fileName to InputFile(fileName, null, File(workDir, inputFileName + ".min.js").absolutePath, module.name) + fileName to InputFile(InputResource.file(fileName), null, File(workDir, inputFileName + ".min.js").absolutePath, module.name) } val testFunctionFqn = testModuleName + (if (testPackage.isNullOrEmpty()) "" else ".$testPackage") + ".$testFunction" diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinJsDcePlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinJsDcePlugin.kt index 14e6329dc4c..9dc087e9d5d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinJsDcePlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinJsDcePlugin.kt @@ -18,11 +18,8 @@ package org.jetbrains.kotlin.gradle.plugin import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.internal.file.UnionFileCollection import org.gradle.api.internal.file.UnionFileTree import org.gradle.api.plugins.JavaPluginConvention -import org.gradle.api.tasks.Copy import org.gradle.api.tasks.SourceSet import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile import org.jetbrains.kotlin.gradle.tasks.KotlinJsDce @@ -33,7 +30,7 @@ class KotlinJsDcePlugin : Plugin { project.pluginManager.apply(Kotlin2JsPluginWrapper::class.java) val javaPluginConvention = project.convention.getPlugin(JavaPluginConvention::class.java) - javaPluginConvention.sourceSets?.forEach { processSourceSet(project, it) } + javaPluginConvention.sourceSets.forEach { processSourceSet(project, it) } } private fun processSourceSet(project: Project, sourceSet: SourceSet) { @@ -42,57 +39,26 @@ class KotlinJsDcePlugin : Plugin { val dceTaskName = sourceSet.getTaskName(DCE_TASK_PREFIX, TASK_SUFFIX) val dceTask = project.tasks.create(dceTaskName, KotlinJsDce::class.java).also { it.dependsOn(kotlinTask) - project.tasks.findByName("build")?.dependsOn(it) + project.tasks.findByName("build").dependsOn(it) } project.afterEvaluate { val outputDir = File(kotlinTask.outputFile).parentFile - val dependenciesDir = File(outputDir, "dependencies") - val dependenciesTemporaryDir = File(outputDir, "dependencies-tmp") - copyDependencies(project, sourceSet, dependenciesDir, dependenciesTemporaryDir, dceTask) - - val dceInputTrees = listOf(project.fileTree(kotlinTask.outputFile), project.fileTree(dependenciesDir)) + val configuration = project.configurations.findByName(sourceSet.compileConfigurationName) + val dceInputTrees = listOf(project.fileTree(kotlinTask.outputFile)) + configuration.map { project.fileTree(it) } val dceInputFiles = UnionFileTree("dce-input", dceInputTrees) with (dceTask) { classpath = sourceSet.compileClasspath destinationDir = File(outputDir, "min") - source(dceInputFiles.filter { it.path.endsWith(".js") }) + source(dceInputFiles) } } } - private fun copyDependencies(project: Project, sourceSet: SourceSet, outputDir: File, tmpDir: File, dceTask: Task) { - val configuration = project.configurations.findByName(sourceSet.compileConfigurationName) ?: return - - val zippedFiles = UnionFileCollection(configuration.map { project.zipTree(it) }) - val files = project.fileTree(tmpDir) - .filter { file -> SUFFIXES.any { file.path.endsWith(it) } } - .filter { file -> SUFFIXES.any { File(file.path.removeSuffix(it) + ".meta.js").exists() } } - - // This intermediate task is needed due to bug in Gradle that causes infinite loops in continuous build mode - val unpackName = sourceSet.getTaskName(UNPACK_DEPENDENCIES_TASK_PREFIX, TASK_SUFFIX) - val unpackTask = project.tasks.create(unpackName, Copy::class.java).apply { - from(zippedFiles) - into(tmpDir) - } - - val name = sourceSet.getTaskName(DEPENDENCIES_TASK_PREFIX, TASK_SUFFIX) - with(project.tasks.create(name, Copy::class.java)) { - from(files) - into(outputDir) - includeEmptyDirs = true - dceTask.dependsOn(this) - dependsOn(unpackTask) - } - } - companion object { private const val TASK_SUFFIX = "kotlinJs" - private const val UNPACK_DEPENDENCIES_TASK_PREFIX = "unpackDependencies" - private const val DEPENDENCIES_TASK_PREFIX = "copyDependencies" private const val DCE_TASK_PREFIX = "runDce" - private val SUFFIXES = listOf(".js", ".js.map") } } \ No newline at end of file