[#5246] Add an org.jooq.Allow.PlainSQL annotation and a PlainSQLChecker using JSR-308 and the checker framework

This commit is contained in:
lukaseder 2016-05-09 13:03:14 +02:00
parent 01bece477c
commit 8897304d42
8 changed files with 234 additions and 74 deletions

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2009-2016, Data Geekery GmbH (http://www.datageekery.com)
* All rights reserved.
*
* 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.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: http://www.jooq.org/licenses
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.checker;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import org.checkerframework.framework.source.Result;
import org.checkerframework.framework.source.SourceChecker;
/**
* Common base class for checkers.
*
* @author Lukas Eder
*/
abstract class AbstractChecker extends SourceChecker {
void error(Object node, String message) {
getChecker().report(Result.failure(message, node), node);
}
void print(Printer printer) {
try (PrintWriter writer = new PrintWriter(new FileWriter("error.txt"))){
writer.println("This is probably a bug in jOOQ-checker.");
writer.println("Please report this bug here: https://github.com/jOOQ/jOOQ/issues/new");
writer.println("---------------------------------------------------------------------");
printer.print(writer);
}
catch (IOException ignore) {}
}
interface Printer {
void print(PrintWriter writer);
}
}

View File

@ -0,0 +1,111 @@
/**
* Copyright (c) 2009-2016, Data Geekery GmbH (http://www.datageekery.com)
* All rights reserved.
*
* 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.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: http://www.jooq.org/licenses
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.checker;
import static com.sun.source.util.TreePath.getPath;
import static org.checkerframework.javacutil.TreeUtils.elementFromDeclaration;
import static org.checkerframework.javacutil.TreeUtils.elementFromUse;
import static org.checkerframework.javacutil.TreeUtils.enclosingMethod;
import java.io.PrintWriter;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import org.jooq.Allow;
import org.jooq.PlainSQL;
import org.checkerframework.framework.source.SourceVisitor;
import com.sun.source.tree.MethodInvocationTree;
/**
* A checker to disallow usage of {@link PlainSQL} API, except where allowed
* explicitly.
*
* @author Lukas Eder
*/
public class PlainSQLChecker extends AbstractChecker {
@Override
protected SourceVisitor<Void, Void> createSourceVisitor() {
return new SourceVisitor<Void, Void>(getChecker()) {
@Override
public Void visitMethodInvocation(MethodInvocationTree node, Void p) {
try {
ExecutableElement elementFromUse = elementFromUse(node);
PlainSQL plainSQL = elementFromUse.getAnnotation(PlainSQL.class);
// In the absence of a @PlainSQL annotation,
// all jOOQ API method calls will type check.
if (plainSQL != null) {
Element enclosing = elementFromDeclaration(enclosingMethod(getPath(root, node)));
boolean allowed = false;
moveUpEnclosingLoop:
while (enclosing != null) {
if (enclosing.getAnnotation(Allow.PlainSQL.class) != null) {
allowed = true;
break moveUpEnclosingLoop;
}
enclosing = enclosing.getEnclosingElement();
}
if (!allowed)
error(node, "Plain SQL usage not allowed at current scope. Use @Allow.PlainSQL.");
}
}
catch (final Exception e) {
print(new Printer() {
@Override
public void print(PrintWriter t) {
e.printStackTrace(t);
}
});
}
return super.visitMethodInvocation(node, p);
}
};
}
}

View File

@ -46,8 +46,6 @@ import static org.checkerframework.javacutil.TreeUtils.elementFromDeclaration;
import static org.checkerframework.javacutil.TreeUtils.elementFromUse;
import static org.checkerframework.javacutil.TreeUtils.enclosingMethod;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.EnumSet;
@ -59,8 +57,6 @@ import org.jooq.Require;
import org.jooq.SQLDialect;
import org.jooq.Support;
import org.checkerframework.framework.source.Result;
import org.checkerframework.framework.source.SourceChecker;
import org.checkerframework.framework.source.SourceVisitor;
import com.sun.source.tree.MethodInvocationTree;
@ -71,11 +67,11 @@ import com.sun.source.tree.MethodInvocationTree;
*
* @author Lukas Eder
*/
public class SQLDialectChecker extends SourceChecker {
public class SQLDialectChecker extends AbstractChecker {
@Override
protected SourceVisitor<?, ?> createSourceVisitor() {
return new SourceVisitor<Void, Void>(this) {
protected SourceVisitor<Void, Void> createSourceVisitor() {
return new SourceVisitor<Void, Void>(getChecker()) {
@Override
public Void visitMethodInvocation(MethodInvocationTree node, Void p) {
@ -134,23 +130,4 @@ public class SQLDialectChecker extends SourceChecker {
}
};
}
void error(Object node, String message) {
getChecker().report(Result.failure(message, node), node);
}
void print(Printer printer) {
try (PrintWriter writer = new PrintWriter(new FileWriter("error.txt"))){
writer.println("This is probably a bug in jOOQ-checker.");
writer.println("Please report this bug here: https://github.com/jOOQ/jOOQ/issues/new");
writer.println("---------------------------------------------------------------------");
printer.print(writer);
}
catch (IOException ignore) {}
}
interface Printer {
void print(PrintWriter writer);
}
}

View File

@ -60,6 +60,7 @@
<fork>true</fork>
<annotationProcessors>
<annotationProcessor>org.jooq.checker.SQLDialectChecker</annotationProcessor>
<annotationProcessor>org.jooq.checker.PlainSQLChecker</annotationProcessor>
</annotationProcessors>
<compilerArgs>
<arg>-Xbootclasspath/p:1.8</arg>

View File

@ -0,0 +1,24 @@
package org.jooq.example.checker;
import org.jooq.Allow;
import org.jooq.impl.DSL;
// This class allows for using PlainSQL API
@Allow.PlainSQL
class PlainSQLCheckerTests1 {
public static void compiles() {
DSL.field("plain SQL");
}
}
// This class doesn't allow for using PlainSQL API
class PlainSQLCheckerTests2 {
public static void doesntCompile() {
DSL.field("plain SQL");
}
@Allow.PlainSQL
public static void compiles() {
DSL.field("plain SQL");
}
}

View File

@ -10,7 +10,7 @@ import org.jooq.impl.DSL;
// The class requires both H2 and MySQL
// The inherited @Allow annotation from the package allows only MySQL, though.
@Require({ SQLDialect.H2, SQLDialect.MYSQL })
public class CheckerTests {
public class SQLDialectCheckerTests {
// @Allow = MySQL (inherited from package)
// @Require = { H2, MySQL } (inherited from class)

View File

@ -1,47 +0,0 @@
package org.jooq.example.checker;
import static org.jooq.example.checker.db.h2.Tables.AUTHOR;
import static org.jooq.example.checker.db.h2.Tables.BOOK;
import java.sql.Connection;
import java.sql.DriverManager;
import org.jooq.Require;
import org.jooq.Result;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
/**
* @author Lukas Eder
*/
@Require(
value = { SQLDialect.H2, SQLDialect.MYSQL }
)
public class ShouldntCompile {
@Require({SQLDialect.H2, SQLDialect.MYSQL})
public static void main(String[] args) throws Exception {
try (Connection c = DriverManager.getConnection("jdbc:h2:~/checker-test", "sa", "")) {
Result<?> result =
DSL.using(c)
.select(
AUTHOR.FIRST_NAME,
AUTHOR.LAST_NAME,
BOOK.ID,
BOOK.TITLE,
DSL.array(1)
)
.from(AUTHOR)
.join(BOOK)
.on(AUTHOR.ID.eq(BOOK.AUTHOR_ID))
.connectBy("abc")
.orderBy(BOOK.ID.asc())
.fetch();
System.out.println(result);
}
}
public static void x() {
DSL.array(2);
}
}

View File

@ -130,6 +130,9 @@ import java.lang.annotation.Target;
* {@link SQLDialect#predecessor()}, and its {@link SQLDialect#family()} are
* allowed.</li>
* </ul>
* <p>
* Apart from the above main purpose, the {@link Allow} annotation also serves
* as a semantic namespace for other annotations, such as {@link Allow.PlainSQL}
*
* @author Lukas Eder
* @see Require
@ -172,4 +175,20 @@ public @interface Allow {
POSTGRES,
SQLITE,
};
/**
* This annotation allows {@link PlainSQL} API usage within the scope of
* where it is placed.
* <p>
* Type checking for these annotations can be supplied by
* <code>org.jooq.checker.PlainSQLChecker</code> from the jOOQ-checker
* module.
*
* @author Lukas Eder
*/
@Target({ METHOD, CONSTRUCTOR, TYPE, PACKAGE })
@Retention(RUNTIME)
@Documented
@Inherited
@interface PlainSQL {}
}