@@ -18,7 +18,6 @@ package org.domaframework.doma.intellij.formatter.visitor
1818import com.intellij.openapi.command.WriteCommandAction
1919import com.intellij.openapi.fileTypes.FileTypeManager
2020import com.intellij.openapi.project.Project
21- import com.intellij.openapi.util.TextRange
2221import com.intellij.psi.JavaRecursiveElementVisitor
2322import com.intellij.psi.PsiDocumentManager
2423import com.intellij.psi.PsiFile
@@ -27,19 +26,30 @@ import com.intellij.psi.PsiLiteralExpression
2726import com.intellij.psi.codeStyle.CodeStyleManager
2827import org.domaframework.doma.intellij.common.util.InjectionSqlUtil
2928import org.domaframework.doma.intellij.common.util.StringUtil
29+ import kotlin.text.isBlank
30+ import kotlin.text.isNotBlank
31+ import kotlin.text.takeWhile
3032
33+ /* *
34+ * Visitor for processing and formatting SQL injections in DAO files.
35+ * Formats SQL strings embedded in Java/Kotlin string literals while preserving indentation.
36+ */
3137class DaoInjectionSqlVisitor (
3238 private val element : PsiFile ,
3339 private val project : Project ,
3440) : JavaRecursiveElementVisitor() {
3541 private data class FormattingTask (
3642 val expression : PsiLiteralExpression ,
3743 val formattedText : String ,
44+ val baseIndent : String ,
3845 )
3946
4047 companion object {
4148 private const val TEMP_FILE_PREFIX = " temp_format"
4249 private const val SQL_FILE_EXTENSION = " .sql"
50+ private const val SQL_COMMENT_PATTERN = " ^[ \\ t]*/\\ *"
51+ private const val TRIPLE_QUOTE = " \"\"\" "
52+ private const val WRITE_COMMAND_NAME = " Format Injected SQL"
4353 }
4454
4555 private val formattingTasks = mutableListOf<FormattingTask >()
@@ -50,33 +60,60 @@ class DaoInjectionSqlVisitor(
5060 if (injected != null ) {
5161 // Format SQL and store the task
5262 val originalSqlText = injected.text
53- val normalizedSql = normalizeIndentation(originalSqlText)
54- val formattedSql = formatAsTemporarySqlFile(normalizedSql)
55- val finalSql = reapplyOriginalIndentation(formattedSql, originalSqlText)
56-
63+ val formattedSql = formatAsTemporarySqlFile(originalSqlText)
64+ // Keep the current top line indent
65+ val baseIndent = getBaseIndent(formattedSql)
5766 val originalText = expression.value?.toString() ? : return
58- if (finalSql != originalText) {
59- formattingTasks.add(FormattingTask (expression, finalSql))
67+
68+ if (formattedSql != originalText) {
69+ formattingTasks.add(FormattingTask (expression, formattedSql, baseIndent))
70+ }
71+ }
72+ }
73+
74+ /* *
75+ * Extracts the base indentation from the first non-blank, non-comment line.
76+ */
77+ private fun getBaseIndent (string : String ): String {
78+ val lines = string.lines()
79+ val commentRegex = Regex (SQL_COMMENT_PATTERN )
80+
81+ // Skip blank lines and comment lines
82+ val firstContentLineIndex =
83+ lines.indexOfFirst { line ->
84+ line.isNotBlank() && ! commentRegex.matches(line)
6085 }
86+
87+ return if (firstContentLineIndex >= 0 ) {
88+ lines[firstContentLineIndex].takeWhile { it.isWhitespace() }
89+ } else {
90+ " "
6191 }
6292 }
6393
94+ /* *
95+ * Processes all collected formatting tasks in a single write action.
96+ * @param removeSpace Function to remove trailing spaces from formatted text
97+ */
6498 fun processAll (removeSpace : (String , Boolean ) -> String ) {
6599 if (formattingTasks.isEmpty()) return
66100
67101 // Apply all formatting tasks in a single write action
68- WriteCommandAction .runWriteCommandAction(project, " Format Injected SQL " , null , {
102+ WriteCommandAction .runWriteCommandAction(project, WRITE_COMMAND_NAME , null , {
69103 // Sort by text range in descending order to maintain offsets
70- formattingTasks.sortedByDescending { it.expression.textRange.startOffset }.forEach { task ->
71- if (task.expression.isValid) {
72- replaceHostStringLiteral(task.expression, task.formattedText, removeSpace)
104+ formattingTasks
105+ .sortedByDescending { it.expression.textRange.startOffset }
106+ .forEach { task ->
107+ if (task.expression.isValid) {
108+ replaceHostStringLiteral(task, removeSpace)
109+ }
73110 }
74- }
75111 })
76112 }
77113
78114 /* *
79- * Execute formatting as a temporary SQL file
115+ * Formats SQL text by creating a temporary SQL file and applying code style.
116+ * Returns original text if formatting fails.
80117 */
81118 private fun formatAsTemporarySqlFile (sqlText : String ): String =
82119 try {
@@ -88,75 +125,99 @@ class DaoInjectionSqlVisitor(
88125 .getInstance(project)
89126 .createFileFromText(tempFileName, fileType, sqlText)
90127
91- val codeStyleManager = CodeStyleManager .getInstance(project)
92- val textRange = TextRange ( 0 , tempSqlFile.textLength )
93- codeStyleManager .reformatText(tempSqlFile, textRange.startOffset, textRange.endOffset )
128+ CodeStyleManager
129+ .getInstance(project )
130+ .reformatText(tempSqlFile, 0 , tempSqlFile.textLength )
94131
95132 tempSqlFile.text
96133 } catch (_: Exception ) {
97- sqlText
134+ sqlText // Return original text on error
98135 }
99136
100137 /* *
101- * Directly replace host Java string literal
138+ * Replaces the host Java string literal with formatted SQL text.
102139 */
103140 private fun replaceHostStringLiteral (
104- literalExpression : PsiLiteralExpression ,
105- formattedSqlText : String ,
141+ task : FormattingTask ,
106142 removeSpace : (String , Boolean ) -> String ,
107143 ) {
108144 try {
109- val normalizedSql = normalizeIndentation(formattedSqlText)
110- val newLiteralText = createFormattedLiteralText(normalizedSql)
111- val removeSpaceText = removeSpace(newLiteralText, false )
112-
113- // Replace PSI element
114- val elementFactory =
115- com.intellij.psi.JavaPsiFacade
116- .getElementFactory(project)
117- val newLiteral = elementFactory.createExpressionFromText(removeSpaceText, literalExpression)
118- val manager = PsiDocumentManager .getInstance(literalExpression.project)
119- val document = manager.getDocument(literalExpression.containingFile) ? : return
120- document.replaceString(literalExpression.textRange.startOffset, literalExpression.textRange.endOffset, newLiteral.text)
145+ val formattedLiteral = createFormattedLiteral(task, removeSpace)
146+ replaceInDocument(task.expression, formattedLiteral)
121147 } catch (_: Exception ) {
122- // Host literal replacement failed: ${e.message}
148+ // Silently ignore formatting failures
123149 }
124150 }
125151
126- private fun normalizeIndentation (sqlText : String ): String {
127- val lines = sqlText.lines()
128- val minIndent =
129- lines
130- .filter { it.isNotBlank() }
131- .minOfOrNull { it.indexOfFirst { char -> ! char.isWhitespace() } } ? : 0
152+ private fun createFormattedLiteral (
153+ task : FormattingTask ,
154+ removeSpace : (String , Boolean ) -> String ,
155+ ): String {
156+ val newLiteralText = createFormattedLiteralText(task.formattedText)
157+ val normalizedText = normalizeIndentation(newLiteralText, task.baseIndent)
158+ val cleanedText = removeSpace(normalizedText, false )
159+
160+ val elementFactory =
161+ com.intellij.psi.JavaPsiFacade
162+ .getElementFactory(project)
163+ val newLiteral = elementFactory.createExpressionFromText(cleanedText, task.expression)
164+ return newLiteral.text
165+ }
132166
133- return lines.joinToString(StringUtil .LINE_SEPARATE ) { line ->
134- if (line.isBlank()) line else line.drop(minIndent)
135- }
167+ private fun replaceInDocument (
168+ expression : PsiLiteralExpression ,
169+ newText : String ,
170+ ) {
171+ val document =
172+ PsiDocumentManager
173+ .getInstance(project)
174+ .getDocument(expression.containingFile) ? : return
175+
176+ val range = expression.textRange
177+ document.replaceString(range.startOffset, range.endOffset, newText)
136178 }
137179
138180 /* *
139- * Create appropriate Java string literal from formatted SQL
181+ * Creates a Java text block (triple-quoted string) from formatted SQL.
140182 */
141183 private fun createFormattedLiteralText (formattedSqlText : String ): String {
142184 val lines = formattedSqlText.split(StringUtil .LINE_SEPARATE )
143- return " \"\"\" ${StringUtil .LINE_SEPARATE }${lines.joinToString(StringUtil .LINE_SEPARATE )} \"\"\" "
185+ return buildString {
186+ append(TRIPLE_QUOTE )
187+ append(StringUtil .LINE_SEPARATE )
188+ append(lines.joinToString(StringUtil .LINE_SEPARATE ))
189+ append(TRIPLE_QUOTE )
190+ }
144191 }
145192
146- private fun reapplyOriginalIndentation (
147- formattedSql : String ,
148- originalSql : String ,
193+ /* *
194+ * Normalizes indentation by removing base indent and reapplying it consistently.
195+ */
196+ private fun normalizeIndentation (
197+ sqlText : String ,
198+ baseIndent : String ,
149199 ): String {
150- val originalLines = originalSql .lines()
151- val formattedLines = formattedSql. lines()
200+ val lines = sqlText .lines()
201+ if ( lines.isEmpty()) return sqlText
152202
153- val originalIndent =
154- originalLines
155- .firstOrNull { it.isNotBlank() }
156- ?.takeWhile { it.isWhitespace() } ? : " "
203+ val literalSeparator = lines.first()
204+ val contentLines = lines.drop(1 )
157205
158- return formattedLines.joinToString(StringUtil .LINE_SEPARATE ) { line ->
159- if (line.isBlank()) line else originalIndent + line
206+ val normalizedLines =
207+ contentLines.map { line ->
208+ when {
209+ line.isBlank() -> line
210+ line.startsWith(baseIndent) -> baseIndent + line.removePrefix(baseIndent)
211+ else -> baseIndent + line
212+ }
213+ }
214+
215+ return buildString {
216+ append(literalSeparator)
217+ if (normalizedLines.isNotEmpty()) {
218+ append(StringUtil .LINE_SEPARATE )
219+ append(normalizedLines.joinToString(StringUtil .LINE_SEPARATE ))
220+ }
160221 }
161222 }
162223}
0 commit comments