diff --git a/src/main/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeOverrider.kt b/src/main/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeOverrider.kt index 5587677..5ff267f 100644 --- a/src/main/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeOverrider.kt +++ b/src/main/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeOverrider.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.fileTypes.impl.FileTypeOverrider import com.intellij.openapi.util.Key import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileWithId class QiqFileTypeOverrider : FileTypeOverrider { @@ -28,23 +29,22 @@ class QiqFileTypeOverrider : FileTypeOverrider { } override fun getOverriddenFileType(file: VirtualFile): FileType? { - // 再入防止 - if (file.getUserData(REENTRANT_GUARD) == true) return null - file.putUserData(REENTRANT_GUARD, true) - try { - if (file.isDirectory) return null + if (file !is VirtualFileWithId) return null + if (file.isDirectory) return null + if (!file.isValid) return null + if (!file.isInLocalFileSystem) return null - // 既に Qiq として扱ったファイルは継続して Qiq を返す(再判定で PHP に戻さない) - if (file.getUserData(QIQ_MARKER) == true) { - return QiqFileType - } + // 既に Qiq として扱ったファイルは継続して Qiq を返す(再判定で PHP に戻さない) + if (file.getUserData(QIQ_MARKER) == true) { + return QiqFileType + } - // 1) Qiq専用拡張子は即スキップ(拡張子のみでQiqに確定させる) - // 注意: file.extension は最後のドット以降のみ(".qiq.php" の extension は "php") - val nameLower = file.nameSequence.toString().lowercase() - if (nameLower.endsWith(".qiq") || nameLower.endsWith(".qiq.php")) { - return null - } + // 1) Qiq専用拡張子は即スキップ(拡張子のみでQiqに確定させる) + // 注意: file.extension は最後のドット以降のみ(".qiq.php" の extension は "php") + val nameLower = file.nameSequence.toString().lowercase() + if (nameLower.endsWith(".qiq") || nameLower.endsWith(".qiq.php")) { + return null + } // 2) 対象候補のみ内容判定(誤検知&無駄I/O削減) val ext = file.extension?.lowercase() @@ -54,12 +54,12 @@ class QiqFileTypeOverrider : FileTypeOverrider { // 3) サイズ上限(巨大ファイルは避ける) if (file.length > SIZE_LIMIT_BYTES) return null + // 再入防止 + if (file.getUserData(REENTRANT_GUARD) == true) return null + file.putUserData(REENTRANT_GUARD, true) + try { // 4) 中身を読む(※ file.fileType は絶対触らない:無限再帰の原因になる) - val text = try { - VfsUtilCore.loadText(file) - } catch (_: Throwable) { - return null - } + val text = runCatching { VfsUtilCore.loadText(file) }.getOrElse { return null } // 5) 軽いチェック if (!quickLooksLikeQiq(text)) return null diff --git a/src/test/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeOverriderTest.kt b/src/test/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeOverriderTest.kt new file mode 100644 index 0000000..e9624fb --- /dev/null +++ b/src/test/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeOverriderTest.kt @@ -0,0 +1,64 @@ +package io.github.jingu.idea_qiq_plugin.lang + +import com.intellij.lang.Language +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.LightVirtualFile +import com.intellij.testFramework.junit5.RunInEdt +import com.intellij.testFramework.junit5.impl.TestApplicationExtension +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.util.Comparator + +@RunInEdt +@ExtendWith(TestApplicationExtension::class) +class QiqFileTypeOverriderTest { + + private val overrider = QiqFileTypeOverrider() + + @Test + fun `does not override LightVirtualFile`() { + val lightFile = LightVirtualFile( + "sample.php", + Language.ANY, + "{{ setSection('header') }}\n{{ endSection() }}" + ) + + val type = overrider.getOverriddenFileType(lightFile) + assertNull(type, "LightVirtualFile must not be overridden as Qiq") + } + + @Test + fun `overrides local virtual file`(@org.junit.jupiter.api.io.TempDir tempDir: java.nio.file.Path) { + val text = """ + {{ setSection('header') }} + content + {{ endSection() }} + """.trimIndent() + + val path = tempDir.resolve("template.php") + Files.writeString(path, text, StandardCharsets.UTF_8) + + val virtualFile = WriteAction.compute { + LocalFileSystem.getInstance().refreshAndFindFileByIoFile(path.toFile()) + } ?: error("Virtual file not found for temp file") + + val type: FileType? = ReadAction.compute { + overrider.getOverriddenFileType(virtualFile) + } + + assertEquals(QiqFileType, type, "Local template should be detected as Qiq") + assertTrue( + virtualFile.getUserData(QiqFileTypeOverrider.QIQ_MARKER) == true, + "Marker flag should be set to avoid repeated re-evaluation" + ) + } +} diff --git a/src/test/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeRecognitionTest.kt b/src/test/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeRecognitionTest.kt index 39ad52e..09b3267 100644 --- a/src/test/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeRecognitionTest.kt +++ b/src/test/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqFileTypeRecognitionTest.kt @@ -1,23 +1,37 @@ package io.github.jingu.idea_qiq_plugin.lang +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.LightVirtualFile -import kotlin.test.Test +import com.intellij.testFramework.junit5.RunInEdt +import com.intellij.testFramework.junit5.impl.TestApplicationExtension +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.util.Comparator import kotlin.test.assertNull import kotlin.test.assertSame import kotlin.test.assertTrue +@RunInEdt +@ExtendWith(TestApplicationExtension::class) class QiqFileTypeRecognitionTest { @Test fun `php template with qiq comment is recognized`() { - val fileContent = "{{ // New Qiq template }}\n" - val virtualFile = LightVirtualFile("new_template.php", fileContent) + withTempVirtualFile("new_template.php", "{{ // New Qiq template }}\n") { file -> + val overrider = QiqFileTypeOverrider() + val overridden = ReadAction.compute { + overrider.getOverriddenFileType(file) + } - val overrider = QiqFileTypeOverrider() - val overridden = overrider.getOverriddenFileType(virtualFile) - - assertSame(QiqFileType, overridden, "PHP template should be recognized as Qiq") - assertTrue(virtualFile.getUserData(QiqFileTypeOverrider.QIQ_MARKER) == true) + assertSame(QiqFileType, overridden, "PHP template should be recognized as Qiq") + assertTrue(file.getUserData(QiqFileTypeOverrider.QIQ_MARKER) == true) + } } @Test @@ -32,13 +46,34 @@ class QiqFileTypeRecognitionTest { @Test fun `missing closing delimiter is ignored`() { - val fileContent = "{{ extends('layout')\n" - val virtualFile = LightVirtualFile("broken_template.php", fileContent) + withTempVirtualFile("broken_template.php", "{{ extends('layout')\n") { file -> + val overrider = QiqFileTypeOverrider() + val overridden = ReadAction.compute { + overrider.getOverriddenFileType(file) + } + + assertNull(overridden, "File without closing delimiter should remain original type") + assertTrue(file.getUserData(QiqFileTypeOverrider.QIQ_MARKER) != true) + } + } + + private fun withTempVirtualFile(name: String, content: String, block: (VirtualFile) -> T): T { + val tempDir = Files.createTempDirectory("qiq-filetype-test") + return try { + val path = tempDir.resolve(name) + Files.writeString(path, content, StandardCharsets.UTF_8) - val overrider = QiqFileTypeOverrider() - val overridden = overrider.getOverriddenFileType(virtualFile) + val virtualFile = WriteAction.compute { + LocalFileSystem.getInstance().refreshAndFindFileByIoFile(path.toFile()) + } ?: error("VirtualFile not found for $name") - assertNull(overridden, "File without closing delimiter should remain original type") - assertTrue(virtualFile.getUserData(QiqFileTypeOverrider.QIQ_MARKER) != true) + block(virtualFile) + } finally { + runCatching { + Files.walk(tempDir).use { stream -> + stream.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) } + } + } + } } }