|
17 | 17 | package org.sonar.python.checks; |
18 | 18 |
|
19 | 19 | import java.util.List; |
| 20 | +import java.util.Locale; |
| 21 | +import java.util.Optional; |
20 | 22 | import java.util.Set; |
21 | 23 | import javax.annotation.CheckForNull; |
22 | 24 | import org.sonar.check.Rule; |
23 | 25 | import org.sonar.plugins.python.api.PythonSubscriptionCheck; |
24 | 26 | import org.sonar.plugins.python.api.SubscriptionContext; |
| 27 | +import org.sonar.plugins.python.api.quickfix.PythonQuickFix; |
| 28 | +import org.sonar.plugins.python.api.tree.Argument; |
25 | 29 | import org.sonar.plugins.python.api.tree.CallExpression; |
26 | 30 | import org.sonar.plugins.python.api.tree.Decorator; |
27 | 31 | import org.sonar.plugins.python.api.tree.Expression; |
28 | 32 | import org.sonar.plugins.python.api.tree.FunctionDef; |
29 | 33 | import org.sonar.plugins.python.api.tree.ListLiteral; |
| 34 | +import org.sonar.plugins.python.api.tree.Name; |
| 35 | +import org.sonar.plugins.python.api.tree.QualifiedExpression; |
30 | 36 | import org.sonar.plugins.python.api.tree.RegularArgument; |
31 | 37 | import org.sonar.plugins.python.api.tree.StringLiteral; |
32 | 38 | import org.sonar.plugins.python.api.tree.Tree; |
33 | 39 | import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher; |
34 | 40 | import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers; |
35 | 41 | import org.sonar.python.checks.utils.Expressions; |
| 42 | +import org.sonar.python.quickfix.TextEditUtils; |
36 | 43 | import org.sonar.python.tree.TreeUtils; |
37 | 44 |
|
38 | 45 | @Rule(key = "S8412") |
39 | 46 | public class FastAPIGenericRouteDecoratorCheck extends PythonSubscriptionCheck { |
40 | 47 |
|
41 | | - private static final String MESSAGE = |
42 | | - "Replace this generic \"route()\" decorator with a specific HTTP method decorator."; |
| 48 | + private static final String MESSAGE = "Replace this generic \"route()\" decorator with a specific HTTP method decorator."; |
| 49 | + |
| 50 | + private static final String QUICK_FIX_MESSAGE = "Replace with \"%s\""; |
43 | 51 |
|
44 | 52 | private static final TypeMatcher ROUTE_DECORATOR_MATCHER = TypeMatchers.any( |
45 | 53 | TypeMatchers.isType("fastapi.applications.FastAPI.route"), |
46 | | - TypeMatchers.isType("fastapi.routing.APIRouter.route") |
47 | | - ); |
| 54 | + TypeMatchers.isType("fastapi.routing.APIRouter.route")); |
48 | 55 |
|
49 | 56 | private static final Set<String> SINGLE_HTTP_METHODS = Set.of( |
50 | 57 | "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE", |
51 | | - "get", "post", "put", "delete", "patch", "options", "head", "trace" |
52 | | - ); |
| 58 | + "get", "post", "put", "delete", "patch", "options", "head", "trace"); |
53 | 59 |
|
54 | 60 | @Override |
55 | 61 | public void initialize(Context context) { |
@@ -81,31 +87,77 @@ private static void checkDecorator(SubscriptionContext ctx, Decorator decorator) |
81 | 87 | } |
82 | 88 |
|
83 | 89 | Expression methodsExpr = methodsArg.expression(); |
84 | | - if (methodsExpr instanceof ListLiteral listLiteral && isSingleHttpMethod(listLiteral)) { |
85 | | - ctx.addIssue(callExpr.callee(), MESSAGE); |
| 90 | + if (!(methodsExpr instanceof ListLiteral listLiteral)) { |
| 91 | + return; |
| 92 | + } |
| 93 | + |
| 94 | + Optional<String> httpMethod = getSingleHttpMethod(listLiteral); |
| 95 | + if (httpMethod.isPresent()) { |
| 96 | + PreciseIssue issue = ctx.addIssue(callExpr.callee(), MESSAGE); |
| 97 | + addQuickFix(issue, callExpr, methodsArg, httpMethod.get()); |
86 | 98 | } |
87 | 99 | } |
88 | 100 |
|
89 | | - private static boolean isSingleHttpMethod(ListLiteral listLiteral) { |
| 101 | + private static Optional<String> getSingleHttpMethod(ListLiteral listLiteral) { |
90 | 102 | List<Expression> elements = listLiteral.elements().expressions(); |
91 | 103 |
|
92 | 104 | if (elements.size() != 1) { |
93 | | - return false; |
| 105 | + return Optional.empty(); |
94 | 106 | } |
95 | 107 |
|
96 | 108 | Expression element = elements.get(0); |
97 | 109 | StringLiteral stringLiteral = Expressions.extractStringLiteral(element); |
98 | 110 | if (stringLiteral == null) { |
99 | | - return false; |
| 111 | + return Optional.empty(); |
100 | 112 | } |
101 | 113 |
|
102 | 114 | String methodName = stringLiteral.trimmedQuotesValue(); |
103 | | - return SINGLE_HTTP_METHODS.contains(methodName); |
| 115 | + if (SINGLE_HTTP_METHODS.contains(methodName)) { |
| 116 | + return Optional.of(methodName.toLowerCase(Locale.ROOT)); |
| 117 | + } |
| 118 | + return Optional.empty(); |
104 | 119 | } |
105 | 120 |
|
106 | 121 | @CheckForNull |
107 | 122 | private static CallExpression getDecoratorCallExpression(Decorator decorator) { |
108 | 123 | Expression decoratorExpr = decorator.expression(); |
109 | 124 | return decoratorExpr instanceof CallExpression callExpr ? callExpr : null; |
110 | 125 | } |
| 126 | + |
| 127 | + private static void addQuickFix(PreciseIssue issue, CallExpression callExpr, |
| 128 | + RegularArgument methodsArg, String httpMethod) { |
| 129 | + Expression callee = callExpr.callee(); |
| 130 | + if (!(callee instanceof QualifiedExpression qualifiedExpr)) { |
| 131 | + return; |
| 132 | + } |
| 133 | + Name routeName = qualifiedExpr.name(); |
| 134 | + |
| 135 | + var builder = PythonQuickFix.newQuickFix(String.format(QUICK_FIX_MESSAGE, httpMethod)); |
| 136 | + builder.addTextEdit(TextEditUtils.replace(routeName, httpMethod)); |
| 137 | + addRemoveMethodArgumentEdit(builder, callExpr, methodsArg); |
| 138 | + |
| 139 | + issue.addQuickFix(builder.build()); |
| 140 | + } |
| 141 | + |
| 142 | + private static void addRemoveMethodArgumentEdit(PythonQuickFix.Builder builder, |
| 143 | + CallExpression callExpr, |
| 144 | + RegularArgument methodsArg) { |
| 145 | + List<Argument> arguments = callExpr.arguments(); |
| 146 | + int argIndex = arguments.indexOf(methodsArg); |
| 147 | + |
| 148 | + if (argIndex == -1) { |
| 149 | + return; |
| 150 | + } |
| 151 | + |
| 152 | + if (arguments.size() == 1) { |
| 153 | + builder.addTextEdit(TextEditUtils.remove(methodsArg)); |
| 154 | + } else if (argIndex == arguments.size() - 1) { |
| 155 | + Argument previousArg = arguments.get(argIndex - 1); |
| 156 | + var lastToken = previousArg.lastToken(); |
| 157 | + builder.addTextEdit(TextEditUtils.replaceRange(lastToken, methodsArg.lastToken(), lastToken.value())); |
| 158 | + } else { |
| 159 | + Argument nextArg = arguments.get(argIndex + 1); |
| 160 | + builder.addTextEdit(TextEditUtils.removeUntil(methodsArg, nextArg)); |
| 161 | + } |
| 162 | + } |
111 | 163 | } |
0 commit comments