diff --git a/zpa-checks/src/integrationTest/resources/expected/oracle-database_23/UnusedVariableCheck.json b/zpa-checks/src/integrationTest/resources/expected/oracle-database_23/UnusedVariableCheck.json index 7a6b3cec..485465a2 100644 --- a/zpa-checks/src/integrationTest/resources/expected/oracle-database_23/UnusedVariableCheck.json +++ b/zpa-checks/src/integrationTest/resources/expected/oracle-database_23/UnusedVariableCheck.json @@ -52,7 +52,7 @@ 5 ], "lnpls/plsql-language-fundamentals-12.sql" : [ - 3 + 4 ], "lnpls/plsql-language-fundamentals-26.sql" : [ 3, @@ -82,9 +82,6 @@ 4, 8 ], - "lnpls/plsql-language-fundamentals-4.sql" : [ - 3 - ], "lnpls/plsql-language-fundamentals-43.sql" : [ 3, 4, @@ -101,9 +98,8 @@ "lnpls/plsql-language-fundamentals-52.sql" : [ 4 ], - "lnpls/plsql-language-fundamentals-8.sql" : [ - 4, - 5 + "lnpls/plsql-language-fundamentals-6.sql" : [ + 3 ], "lnpls/plsql-optimization-and-tuning-106.sql" : [ 20, diff --git a/zpa-checks/src/integrationTest/resources/expected/oracle-database_23/VariableHidingCheck.json b/zpa-checks/src/integrationTest/resources/expected/oracle-database_23/VariableHidingCheck.json index 86dca26c..9f19a204 100644 --- a/zpa-checks/src/integrationTest/resources/expected/oracle-database_23/VariableHidingCheck.json +++ b/zpa-checks/src/integrationTest/resources/expected/oracle-database_23/VariableHidingCheck.json @@ -16,9 +16,5 @@ ], "lnpls/plsql-language-fundamentals-52.sql" : [ 7 - ], - "lnpls/plsql-language-fundamentals-8.sql" : [ - 3, - 4 ] } \ No newline at end of file diff --git a/zpa-checks/src/test/resources/checks/unused_cursor.sql b/zpa-checks/src/test/resources/checks/unused_cursor.sql index 75505f06..9f2ee72d 100644 --- a/zpa-checks/src/test/resources/checks/unused_cursor.sql +++ b/zpa-checks/src/test/resources/checks/unused_cursor.sql @@ -2,10 +2,10 @@ declare cursor cur is -- Noncompliant {{Remove this unused "CUR" cursor.}} select 1 from dual; - cursor cur2 is + cursor "cur2" is select 1 from dual; begin - open cur2; + open "cur2"; end; create package pkg is diff --git a/zpa-checks/src/test/resources/checks/unused_parameter.sql b/zpa-checks/src/test/resources/checks/unused_parameter.sql index d6b17fee..4bb0dad3 100644 --- a/zpa-checks/src/test/resources/checks/unused_parameter.sql +++ b/zpa-checks/src/test/resources/checks/unused_parameter.sql @@ -1,7 +1,8 @@ -create procedure foo(a number, b number) is -- Noncompliant {{Remove this unused "B" parameter.}} +create procedure foo(a number, b number, "c" number) is -- Noncompliant {{Remove this unused "B" parameter.}} -- ^^^^^^^^ begin print(a); + print("c"); end; / diff --git a/zpa-checks/src/test/resources/checks/unused_variable.sql b/zpa-checks/src/test/resources/checks/unused_variable.sql index c298ccd9..d8028214 100644 --- a/zpa-checks/src/test/resources/checks/unused_variable.sql +++ b/zpa-checks/src/test/resources/checks/unused_variable.sql @@ -31,11 +31,16 @@ end; create or replace package body test is package_body_var number; -- Noncompliant {{Remove this unused "PACKAGE_BODY_VAR" local variable.}} hidden_var number; -- Noncompliant {{Remove this unused "HIDDEN_VAR" local variable.}} + "VAR" number; -- Noncompliant {{Remove this unused "VAR" local variable.}} + "var" number; procedure proc is hidden_var number; -- this declaration hides the previous one + var number; -- this declaration hides the previous "VAR" begin hidden_var := 0; + var := 0; + "var" := 0; end; end; / diff --git a/zpa-checks/src/test/resources/checks/variable_hiding.sql b/zpa-checks/src/test/resources/checks/variable_hiding.sql index 90709ed8..7f084467 100644 --- a/zpa-checks/src/test/resources/checks/variable_hiding.sql +++ b/zpa-checks/src/test/resources/checks/variable_hiding.sql @@ -3,6 +3,7 @@ declare var2 number; var3 number; exc exception; + "QUOTEDVAR" number; procedure test is var number; -- Noncompliant {{This variable "VAR" hides the declaration on line 2.}} [[secondary=2]] @@ -29,6 +30,8 @@ begin declare var3 number; -- Noncompliant {{This variable "VAR3" hides the declaration on line 4.}} [[secondary=4]] -- ^^^^ + quotedvar number; -- Noncompliant {{This variable "QUOTEDVAR" hides the declaration on line 6.}} [[secondary=6]] + "QuotedVar" number; -- this is a different variable begin null; end; @@ -39,12 +42,12 @@ create package body test is var number; procedure test is - var number; -- Noncompliant {{This variable "VAR" hides the declaration on line 39.}} [[secondary=39]] + var number; -- Noncompliant {{This variable "VAR" hides the declaration on line 42.}} [[secondary=42]] -- ^^^ begin for i in 1..10 loop declare - i number; -- Noncompliant {{This variable "I" hides the declaration on line 45.}} [[secondary=45]] + i number; -- Noncompliant {{This variable "I" hides the declaration on line 48.}} [[secondary=48]] -- ^ begin null; @@ -60,14 +63,17 @@ create package test2 is end; / create package body test2 is - var number; -- Noncompliant {{This variable "VAR" hides the declaration on line 58.}} [[secondary=58]] - exc exception; -- Noncompliant {{This variable "EXC" hides the declaration on line 59.}} [[secondary=59]] + var number; -- Noncompliant {{This variable "VAR" hides the declaration on line 61.}} [[secondary=61]] + exc exception; -- Noncompliant {{This variable "EXC" hides the declaration on line 62.}} [[secondary=62]] end; / -- correct declare - outer_var number; + outer_var number; + "VAR2" number; --| + "Var2" number; --| These variables are not the same + "var2" number; --| begin declare var number; diff --git a/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/IdentifierChannel.kt b/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/IdentifierChannel.kt index e515b1b6..b2f57544 100644 --- a/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/IdentifierChannel.kt +++ b/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/IdentifierChannel.kt @@ -29,7 +29,7 @@ class IdentifierChannel(private val channel: IdentifierAndKeywordChannel) override fun consume(code: CodeReader, output: LexerOutput): Boolean { val nextChar = code.peek().toChar() - if (!nextChar.isLetter() && nextChar != '"') { + if (!nextChar.isLetter()) { return false } diff --git a/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/PlSqlLexer.kt b/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/PlSqlLexer.kt index 16efa6e2..c18875fb 100644 --- a/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/PlSqlLexer.kt +++ b/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/PlSqlLexer.kt @@ -72,9 +72,10 @@ object PlSqlLexer { .withChannel(StringChannel(regexp(PlSqlTokenType.STRING_LITERAL, STRING_LITERAL))) .withChannel(DateChannel(regexp(PlSqlTokenType.DATE_LITERAL, DATE_LITERAL))) .withChannel(DateChannel(regexp(PlSqlTokenType.TIMESTAMP_LITERAL, TIMESTAMP_LITERAL))) - .withChannel(IdentifierChannel(IdentifierAndKeywordChannel(or(SIMPLE_IDENTIFIER, QUOTED_IDENTIFIER), false, + .withChannel(IdentifierChannel(IdentifierAndKeywordChannel(SIMPLE_IDENTIFIER, false, PlSqlKeyword.entries.toTypedArray() ))) + .withChannel(QuotedIdentifierChannel(QUOTED_IDENTIFIER, SIMPLE_IDENTIFIER)) .withChannel(PunctuatorChannel(*PlSqlPunctuator.entries.toTypedArray())) .withChannel(BlackHoleChannel("(?is)" + or( "\\s&&?$SIMPLE_IDENTIFIER", diff --git a/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/QuotedIdentifierChannel.kt b/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/QuotedIdentifierChannel.kt new file mode 100644 index 00000000..77e5dae0 --- /dev/null +++ b/zpa-core/src/main/kotlin/org/sonar/plsqlopen/lexer/QuotedIdentifierChannel.kt @@ -0,0 +1,64 @@ +/** + * Z PL/SQL Analyzer + * Copyright (C) 2015-2024 Felipe Zorzo + * mailto:felipe AT felipezorzo DOT com DOT br + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plsqlopen.lexer + +import com.felipebz.flr.api.GenericTokenType +import com.felipebz.flr.api.Token +import com.felipebz.flr.channel.Channel +import com.felipebz.flr.channel.CodeReader +import com.felipebz.flr.impl.LexerOutput +import java.util.regex.Pattern + + +class QuotedIdentifierChannel(quotedIdentifierRegexp: String, simpleIdentifierRegexp: String) : Channel { + + private val quotedPattern = Pattern.compile(quotedIdentifierRegexp) + private val quotedSimplePattern = Pattern.compile(""""$simpleIdentifierRegexp"""") + + override fun consume(code: CodeReader, output: LexerOutput): Boolean { + val nextChar = code.peek().toChar() + if (nextChar != '"') { + return false + } + + val tmpBuilder = StringBuilder() + val matcher = quotedPattern.matcher("") + + if (code.popTo(matcher, tmpBuilder) > 0) { + var word = tmpBuilder.toString() + val wordOriginal = word + if (quotedSimplePattern.matcher(word).matches() && word == word.uppercase()) { + word = word.substring(1, word.length - 1) + } + + val token = Token.builder() + .setType(GenericTokenType.IDENTIFIER) + .setValueAndOriginalValue(word, wordOriginal) + .setLine(code.previousCursor.line) + .setColumn(code.previousCursor.column) + .build() + output.addToken(token) + return true + } + + return false + } + +} diff --git a/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/symbols/Symbol.kt b/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/symbols/Symbol.kt index e41ad5e2..5a749df8 100644 --- a/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/symbols/Symbol.kt +++ b/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/symbols/Symbol.kt @@ -85,7 +85,11 @@ open class Symbol(val node: AstNode?, fun `is`(kind: Kind) = kind == this.kind - fun called(name: String) = name.equals(this.name, ignoreCase = true) + fun called(name: String) = if (name.startsWith('"')) { + name == this.name + } else { + name.equals(this.name, ignoreCase = true) + } override fun toString() = "Symbol name=$name kind=$kind datatype=$datatype" } diff --git a/zpa-core/src/test/kotlin/org/sonar/plsqlopen/lexer/QuotedIdentifierChannelTest.kt b/zpa-core/src/test/kotlin/org/sonar/plsqlopen/lexer/QuotedIdentifierChannelTest.kt new file mode 100644 index 00000000..53ae411a --- /dev/null +++ b/zpa-core/src/test/kotlin/org/sonar/plsqlopen/lexer/QuotedIdentifierChannelTest.kt @@ -0,0 +1,47 @@ +/** + * Z PL/SQL Analyzer + * Copyright (C) 2015-2024 Felipe Zorzo + * mailto:felipe AT felipezorzo DOT com DOT br + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plsqlopen.lexer + +import com.felipebz.flr.api.GenericTokenType +import com.felipebz.flr.impl.LexerOutput +import com.felipebz.flr.tests.Assertions.assertThat +import org.junit.jupiter.api.Test + +class QuotedIdentifierChannelTest { + private val channel = QuotedIdentifierChannel("\".*?\"", "[A-Z]+") + private val output = LexerOutput() + + @Test + fun doesNotConsumeUnquotedWord() { + assertThat(channel).doesNotConsume("word", output) + } + + @Test + fun consumeQuotedWord() { + assertThat(channel).consume("\"some word\"", output) + assertThat(output.tokens).hasToken("\"some word\"", GenericTokenType.IDENTIFIER) + } + + @Test + fun consumeQuotedWordWithSimpleIdentifier() { + assertThat(channel).consume("\"WORD\"", output) + assertThat(output.tokens).hasToken("WORD", GenericTokenType.IDENTIFIER) + } +} diff --git a/zpa-core/src/test/kotlin/org/sonar/plsqlopen/symbols/SymbolVisitorTest.kt b/zpa-core/src/test/kotlin/org/sonar/plsqlopen/symbols/SymbolVisitorTest.kt index 491164f1..75d7ed9a 100644 --- a/zpa-core/src/test/kotlin/org/sonar/plsqlopen/symbols/SymbolVisitorTest.kt +++ b/zpa-core/src/test/kotlin/org/sonar/plsqlopen/symbols/SymbolVisitorTest.kt @@ -549,6 +549,47 @@ end; assertThat(x.innerScope).isNull() } + @Test + fun symbolsWithQuotedIdentifiers() { + val symbols = scan( + """ +declare + "VAR" number; + "Var" number; + "var" number; +begin + var := 1; + "VAR" := 1; + "Var" := 1; + "var" := 1; +end; +""" + ) + assertThat(symbols).hasSize(3) + + val var1 = symbols.find("var", 2, 3) + assertThat(var1.type).isEqualTo(PlSqlType.NUMERIC) + assertThat(var1.references).containsExactly( + tuple(6, 3), + tuple(7, 3), + ) + assertThat(var1.innerScope).isNull() + + val var2 = symbols.find("\"Var\"", 3, 3) + assertThat(var2.type).isEqualTo(PlSqlType.NUMERIC) + assertThat(var2.references).containsExactly( + tuple(8, 3), + ) + assertThat(var2.innerScope).isNull() + + val var3 = symbols.find("\"var\"", 4, 3) + assertThat(var3.type).isEqualTo(PlSqlType.NUMERIC) + assertThat(var3.references).containsExactly( + tuple(9, 3), + ) + assertThat(var3.innerScope).isNull() + } + private fun scan(contents: String): List { val file = tempFolder.resolve("test.sql") file.writeText(contents.trim())