Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hyperlinks of stack traces in Laravel logs #148

Merged
merged 5 commits into from
Dec 3, 2023
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
@@ -0,0 +1,71 @@
package com.intellij.ideolog.filters

import com.intellij.execution.filters.Filter
import com.intellij.execution.filters.HyperlinkInfo
import com.intellij.execution.filters.LazyFileHyperlinkInfo
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.LocalFileSystem
import java.util.regex.Matcher
import java.util.regex.Pattern

class LaravelStackTraceFileFilter(
private val project: Project,
private val localFileSystem: LocalFileSystem
) : Filter, DumbAware {
companion object {
private const val FILE_LINE_REGEX = "\\(\\d+\\)"
private val LINUX_MACOS_FILE_PATTERN =
Pattern.compile("""\B/(?:[\w.-]+/)*[\w.-]+$FILE_LINE_REGEX""")
private val WINDOWS_FILE_PATTERN =
Pattern.compile("""\b[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$FILE_LINE_REGEX""")

private fun canContainFilePathFromLaravelLogs(line: String): Boolean {
return (line.contains(":\\") || line.contains('/')) && line.contains('(') && line.contains(')')
}
}

override fun applyFilter(line: String, entireLength: Int): Filter.Result? {
if (!canContainFilePathFromLaravelLogs(line)) return null

val textStartOffset = entireLength - line.length
val bombedCharSequence = StringUtil.newBombedCharSequence(line, 100)
val filterResultItems = collectFilterResultItems(textStartOffset, LINUX_MACOS_FILE_PATTERN.matcher(bombedCharSequence)) +
collectFilterResultItems(textStartOffset, WINDOWS_FILE_PATTERN.matcher(bombedCharSequence))

return when (filterResultItems.size) {
0 -> null
1 -> Filter.Result(filterResultItems[0].highlightStartOffset, filterResultItems[0].highlightEndOffset, filterResultItems[0].hyperlinkInfo)
else -> Filter.Result(filterResultItems)
}
}

private fun collectFilterResultItems(textStartOffset: Int, matcher: Matcher): List<Filter.ResultItem> {
val resultItems = mutableListOf<Filter.ResultItem>()
while (matcher.find()) {
resultItems.add(Filter.ResultItem(
textStartOffset + matcher.start(),
textStartOffset + matcher.end(),
buildFileHyperlinkInfo(matcher.group()))
)
}
return resultItems
}

private fun buildFileHyperlinkInfo(fileUri: String): HyperlinkInfo? {
val filePathEndIndex = fileUri.lastIndexOf('(')
val filePath = fileUri.substring(0, filePathEndIndex)
localFileSystem.findFileByPath(filePath) ?: return null

val possibleDocumentLine = StringUtil.parseInt(fileUri.substring(filePathEndIndex + 1, fileUri.lastIndex), Int.MIN_VALUE)
val documentLine = if (possibleDocumentLine != Int.MIN_VALUE) possibleDocumentLine - 1 else 0
return LinedFileHyperlinkInfo(project, filePath, documentLine)
}

class LinedFileHyperlinkInfo(
project: Project,
val filePath: String,
val documentLine: Int,
) : LazyFileHyperlinkInfo(project, filePath, documentLine, 0, false)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.intellij.ideolog.filters

import com.intellij.execution.filters.ConsoleFilterProvider
import com.intellij.execution.filters.Filter
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem

class LaravelStackTraceFileFilterProvider : ConsoleFilterProvider {
override fun getDefaultFilters(project: Project): Array<Filter> {
return arrayOf(LaravelStackTraceFileFilter(project, LocalFileSystem.getInstance()))
}
}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<editorHighlighterProvider filetype="Log" implementationClass="com.intellij.ideolog.highlighting.LogFileEditorHighlighterProvider" />
<extendWordSelectionHandler implementation="com.intellij.ideolog.editorActions.ExtendsSelection"/>
<fileEditorProvider implementation="com.intellij.ideolog.file.LogFileEditorProvider" />
<consoleFilterProvider implementation="com.intellij.ideolog.filters.LaravelStackTraceFileFilterProvider"/>
<!--suppress PluginXmlValidity -->
<lang.parserDefinition language="LOG" implementationClass="com.intellij.ideolog.psi.LogFileParserDefinition"/>
<!--suppress PluginXmlValidity -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.intellij.ideolog.filters

import com.intellij.execution.filters.Filter
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.testFramework.RunsInEdt
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import junit.framework.TestCase
import org.junit.rules.TemporaryFolder

@RunsInEdt
internal class LaravelStackTraceFileFilterTests : BasePlatformTestCase() {

private lateinit var filter: LaravelStackTraceFileFilter
private val tmpFolder: TemporaryFolder = TemporaryFolder()

override fun setUp() {
super.setUp()
tmpFolder.create()
filter = LaravelStackTraceFileFilter(project, LocalFileSystem.getInstance())
}

override fun tearDown() {
super.tearDown()
tmpFolder.delete()
}

fun `test no file hyperlink`() {
assertNoFileHyperlink("")
assertNoFileHyperlink("No file hyperlink")
assertNoFileHyperlink("""/Users\me/Application.php""")
assertNoFileHyperlink("""C:\Users\me/Application.php""")
}

fun `test no Laravel logs file hyperlink`() {
assertNoFileHyperlink("/Users/me/Application.php:35")
assertNoFileHyperlink("file:///Users/me/Application.php:35")
assertNoFileHyperlink("""C:\Users\me\Application.php:35""")
}

fun `test single Laravel logs file hyperlink`() {
assertFileHyperlink("/Users/me/Application.php(35)", 0, 29, "/Users/me/Application.php", 35, false)
assertFileHyperlink("""C:\Users\me\Application.php(34)""", 0, 31, """C:\Users\me\Application.php""", 34, false)
}

fun `test multiple Laravel logs file hyperlinks`() {
assertFileHyperlinks(
applyFilter("{ #stacktrace: /Users/me/Application.php(35) /Users/me/Kernel.php(42) }"),
listOf(
FileLinkInfo(15, 44, "/Users/me/Application.php", 35, false),
FileLinkInfo(45, 69, "/Users/me/Kernel.php", 42, false)
)
)
}

fun `test apply Filter to existing Laravel file path on linux or mac`() {
val existingFile = tmpFolder.newFile("Application.php")
val filePathLength = existingFile.absolutePath.length
assertFileHyperlink("#0 ${existingFile.absolutePath}(2)", 3, 6 + filePathLength, existingFile.absolutePath, 2, true)
}

fun `test apply Filter to multiple existing Laravel files path on linux or mac`() {
val firstExistingFile = tmpFolder.newFile("Application.php")
val firstFilePathLength = firstExistingFile.absolutePath.length
val secondExistingFile = tmpFolder.newFile("Kernel.php")
val secondFilePathLength = secondExistingFile.absolutePath.length
assertFileHyperlinks(
applyFilter("#0 ${firstExistingFile.absolutePath}(2), ${secondExistingFile.absolutePath}(4)"),
listOf(
FileLinkInfo(3, 6 + firstFilePathLength, firstExistingFile.absolutePath, 2, true),
FileLinkInfo(8 + firstFilePathLength, 11 + firstFilePathLength + secondFilePathLength, secondExistingFile.absolutePath, 4, true)
)
)
}

private fun applyFilter(line: String) = filter.applyFilter(line, line.length)

private fun assertNoFileHyperlink(text: String) {
assertNull(applyFilter(text))
}

private fun assertFileHyperlink(
text: String,
highlightStartOffset: Int,
highlightEndOffset: Int,
filePath: String,
documentLine: Int,
isFileExists: Boolean
) {
assertFileHyperlinks(
applyFilter(text),
listOf(FileLinkInfo(highlightStartOffset, highlightEndOffset, filePath, documentLine, isFileExists))
)
}

private fun assertFileHyperlinks(result: Filter.Result?, infos: List<FileLinkInfo>) {
assertNotNull(result)
result?.let {
val items = result.resultItems
assertEquals(infos.size, items.size)
infos.indices.forEach { assertHyperlink(items[it], infos[it]) }
}
}

private fun assertHyperlink(actualItem: Filter.ResultItem, expectedFileLinkInfo: FileLinkInfo) {
assertEquals(expectedFileLinkInfo.highlightStartOffset, actualItem.highlightStartOffset)
assertEquals(expectedFileLinkInfo.highlightEndOffset, actualItem.highlightEndOffset)
if (expectedFileLinkInfo.isFileExists) {
assertInstanceOf(actualItem.hyperlinkInfo, LaravelStackTraceFileFilter.LinedFileHyperlinkInfo::class.java)
assertFileLink(expectedFileLinkInfo, actualItem.hyperlinkInfo as LaravelStackTraceFileFilter.LinedFileHyperlinkInfo)
} else {
TestCase.assertNull(actualItem.hyperlinkInfo)
}
}

private fun assertFileLink(expected: FileLinkInfo, actual: LaravelStackTraceFileFilter.LinedFileHyperlinkInfo) {
assertEquals(expected.filePath, actual.filePath)
assertEquals(expected.line, actual.documentLine + 1)
}

data class FileLinkInfo(
val highlightStartOffset: Int,
val highlightEndOffset: Int,
val filePath: String,
val line: Int,
val isFileExists: Boolean
)
}