Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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()
Expand All @@ -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 {
Comment thread
jingu marked this conversation as resolved.
// 4) 中身を読む(※ file.fileType は絶対触らない:無限再帰の原因になる)
Comment thread
jingu marked this conversation as resolved.
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<VirtualFile?, RuntimeException> {
LocalFileSystem.getInstance().refreshAndFindFileByIoFile(path.toFile())
} ?: error("Virtual file not found for temp file")

val type: FileType? = ReadAction.compute<FileType?, RuntimeException> {
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"
)
}
}
Original file line number Diff line number Diff line change
@@ -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<FileType?, RuntimeException> {
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
Expand All @@ -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<FileType?, RuntimeException> {
overrider.getOverriddenFileType(file)
}

assertNull(overridden, "File without closing delimiter should remain original type")
assertTrue(file.getUserData(QiqFileTypeOverrider.QIQ_MARKER) != true)
}
}

private fun <T> 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<VirtualFile?, RuntimeException> {
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) }
}
}
}
}
}