Skip to content
This repository was archived by the owner on Aug 17, 2020. It is now read-only.
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
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ buildscript {
'minSdk': 14,
'compileSdk': 27,
'kotlin': '1.1.60',
'lint': '26.0.1'
]

repositories {
Expand All @@ -21,6 +22,7 @@ allprojects {
repositories {
mavenCentral()
google()
jcenter()
}

group = GROUP
Expand Down Expand Up @@ -50,6 +52,11 @@ ext {
rxBinding = 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
junit = 'junit:junit:4.12'
truth = 'com.google.truth:truth:0.36'

// Lint dependencies.
lintApi = "com.android.tools.lint:lint-api:${versions.lint}"
lint = "com.android.tools.lint:lint:${versions.lint}"
lintTests = "com.android.tools.lint:lint-tests:${versions.lint}"
}

configurations {
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
include ':sqlbrite'
include ':sqlbrite-kotlin'
include ':sqlbrite-lint'
include ':sample'

rootProject.name = 'sqlbrite-root'
16 changes: 16 additions & 0 deletions sqlbrite-lint/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apply plugin: 'kotlin'

dependencies {
compileOnly rootProject.ext.kotlinStdLib

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be implementation since, in theory, future versions of lint could not be based on Kotlin and thus not have the stdlib already on the classpath.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I mark it as implementation instead of compileOnly I have this error:

Execution failed for task :sqlbrite:prepareLintJar.
Found more than one jar in the 'lintChecks' configuration. Only one file is supported. If using a separate Gradle project, make sure compilation dependencies are using compileOnly

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's... interesting. I mean it makes sense, but I guess we have to assume that lint will always contain the Kotlin stdlib as a result.

compileOnly rootProject.ext.lintApi

testImplementation rootProject.ext.junit
testImplementation rootProject.ext.lint
testImplementation rootProject.ext.lintTests
}

jar {
manifest {
attributes("Lint-Registry-v2": "com.squareup.sqlbrite3.BriteIssueRegistry")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2017 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.sqlbrite3

import com.android.tools.lint.client.api.IssueRegistry

class BriteIssueRegistry : IssueRegistry() {

override fun getIssues() = listOf(SqlBriteArgCountDetector.ISSUE)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2017 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.sqlbrite3

import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.ConstantEvaluator.evaluateString
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope.JAVA_FILE
import com.android.tools.lint.detector.api.Scope.TEST_SOURCES
import com.android.tools.lint.detector.api.Severity
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import java.util.EnumSet

private const val BRITE_DATABASE = "com.squareup.sqlbrite3.BriteDatabase"
private const val QUERY_METHOD_NAME = "query"
private const val CREATE_QUERY_METHOD_NAME = "createQuery"

class SqlBriteArgCountDetector : Detector(), Detector.UastScanner {

companion object {

val ISSUE: Issue = Issue.create(
"SqlBriteArgCount",
"Number of provided arguments doesn't match number " +
"of arguments specified in query",
"When providing arguments to query you need to provide the same amount of " +
"arguments that is specified in query.",
Category.MESSAGES,
9,
Severity.ERROR,
Implementation(SqlBriteArgCountDetector::class.java, EnumSet.of(JAVA_FILE, TEST_SOURCES)))
}

override fun getApplicableMethodNames() = listOf(CREATE_QUERY_METHOD_NAME, QUERY_METHOD_NAME)

override fun visitMethod(context: JavaContext, call: UCallExpression, method: PsiMethod) {
val evaluator = context.evaluator

if (evaluator.isMemberInClass(method, BRITE_DATABASE)) {
// Skip non varargs overloads.
if (!method.isVarArgs) return

// Position of sql parameter depends on method.
val sql = evaluateString(context,
call.valueArguments[if (call.isQueryMethod()) 0 else 1], true) ?: return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice


// Count only vararg arguments.
val argumentsCount = call.valueArgumentCount - if (call.isQueryMethod()) 1 else 2
val questionMarksCount = sql.count { it == '?' }
if (argumentsCount != questionMarksCount) {
val requiredArguments = "$questionMarksCount ${"argument".pluralize(questionMarksCount)}"
val actualArguments = "$argumentsCount ${"argument".pluralize(argumentsCount)}"
context.report(ISSUE, call, context.getLocation(call), "Wrong argument count, " +
"query $sql requires $requiredArguments, but was provided $actualArguments")
}
}
}

private fun UCallExpression.isQueryMethod() = methodName == QUERY_METHOD_NAME

private fun String.pluralize(count: Int) = if (count == 1) this else this + "s"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already this function in LintUtils.pluralize but unfortuantely it's private :( we could request it to be made public

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
* Copyright (C) 2017 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.sqlbrite3

import com.android.tools.lint.checks.infrastructure.TestFiles.java
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import org.junit.Test

class SqlBriteArgCountDetectorTest {

companion object {
private val BRITE_DATABASE_STUB = java(
"""
package com.squareup.sqlbrite3;

public final class BriteDatabase {

public void query(String sql, Object... args) {
}

public void createQuery(String table, String sql, Object... args) {
}

// simulate createQuery with SupportSQLiteQuery query parameter
public void createQuery(String table, int something) {
}
}
""".trimIndent()
)
}

@Test
fun cleanCaseWithWithQueryAsLiteral() {
lint().files(
BRITE_DATABASE_STUB,
java(
"""
package test.pkg;

import com.squareup.sqlbrite3.BriteDatabase;

public class Test {
private static final String QUERY = "SELECT name FROM table WHERE id = ?";

public void test() {
BriteDatabase db = new BriteDatabase();
db.query(QUERY, "id");
}

}
""".trimIndent()))
.issues(SqlBriteArgCountDetector.ISSUE)
.run()
.expectClean()
}

@Test
fun cleanCaseWithQueryAsBinaryExpression() {
lint().files(
BRITE_DATABASE_STUB,
java(
"""
package test.pkg;

import com.squareup.sqlbrite3.BriteDatabase;

public class Test {
private static final String QUERY = "SELECT name FROM table WHERE ";

public void test() {
BriteDatabase db = new BriteDatabase();
db.query(QUERY + "id = ?", "id");
}

}
""".trimIndent()))
.issues(SqlBriteArgCountDetector.ISSUE)
.run()
.expectClean()
}

@Test
fun cleanCaseWithQueryThatCantBeEvaluated() {
lint().files(
BRITE_DATABASE_STUB,
java(
"""
package test.pkg;

import com.squareup.sqlbrite3.BriteDatabase;

public class Test {
private static final String QUERY = "SELECT name FROM table WHERE id = ?";

public void test() {
BriteDatabase db = new BriteDatabase();
db.query(query(), "id");
}

private String query() {
return QUERY + " age = ?";
}

}
""".trimIndent()))
.issues(SqlBriteArgCountDetector.ISSUE)
.run()
.expectClean()
}

@Test
fun cleanCaseWithNonVarargMethodCall() {
lint().files(
BRITE_DATABASE_STUB,
java(
"""
package test.pkg;

import com.squareup.sqlbrite3.BriteDatabase;

public class Test {

public void test() {
BriteDatabase db = new BriteDatabase();
db.createQuery("table", 42);
}

}
""".trimIndent()))
.issues(SqlBriteArgCountDetector.ISSUE)
.run()
.expectClean()
}

@Test
fun queryMethodWithWrongNumberOfArguments() {
lint().files(
BRITE_DATABASE_STUB,
java(
"""
package test.pkg;

import com.squareup.sqlbrite3.BriteDatabase;

public class Test {
private static final String QUERY = "SELECT name FROM table WHERE id = ?";

public void test() {
BriteDatabase db = new BriteDatabase();
db.query(QUERY);
}

}
""".trimIndent()))
.issues(SqlBriteArgCountDetector.ISSUE)
.run()
.expect("src/test/pkg/Test.java:10: " +
"Error: Wrong argument count, query SELECT name FROM table WHERE id = ?" +
" requires 1 argument, but was provided 0 arguments [SqlBriteArgCount]\n" +
" db.query(QUERY);\n" +
" ~~~~~~~~~~~~~~~\n" +
"1 errors, 0 warnings")
}

@Test
fun createQueryMethodWithWrongNumberOfArguments() {
lint().files(
BRITE_DATABASE_STUB,
java(
"""
package test.pkg;

import com.squareup.sqlbrite3.BriteDatabase;

public class Test {
private static final String QUERY = "SELECT name FROM table WHERE id = ?";

public void test() {
BriteDatabase db = new BriteDatabase();
db.createQuery("table", QUERY);
}

}
""".trimIndent()))
.issues(SqlBriteArgCountDetector.ISSUE)
.run()
.expect("src/test/pkg/Test.java:10: " +
"Error: Wrong argument count, query SELECT name FROM table WHERE id = ?" +
" requires 1 argument, but was provided 0 arguments [SqlBriteArgCount]\n" +
" db.createQuery(\"table\", QUERY);\n" +
" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" +
"1 errors, 0 warnings")
}
}
2 changes: 2 additions & 0 deletions sqlbrite/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ dependencies {
androidTestImplementation rootProject.ext.supportTestRunner
androidTestImplementation rootProject.ext.truth
androidTestImplementation rootProject.ext.supportSqliteFramework

lintChecks project(':sqlbrite-lint')
}

android {
Expand Down