checkClasses() {
+ return Arrays.asList(CustomPythonVisitorCheck.class, CustomPythonSubscriptionCheck.class);
+ }
+
+ private String loadResource(String path) {
+ URL resource = getClass().getResource(path);
+ if (resource == null) {
+ throw new IllegalStateException("Resource not found: " + path);
+ }
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ try (InputStream in = resource.openStream()) {
+ byte[] buffer = new byte[1024];
+ for (int len = in.read(buffer); len != -1; len = in.read(buffer)) {
+ result.write(buffer, 0, len);
+ }
+ return new String(result.toByteArray(), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to read resource: " + path, e);
+ }
+ }
+}
diff --git a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/CustomPythonRulesPlugin.java b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/CustomPythonRulesPlugin.java
new file mode 100644
index 0000000000..c64f507085
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/CustomPythonRulesPlugin.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2011-2024 SonarSource SA - mailto:info AT sonarsource DOT com
+ * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
+ */
+package org.sonar.samples.python;
+
+import org.sonar.api.Plugin;
+
+public class CustomPythonRulesPlugin implements Plugin {
+
+ @Override
+ public void define(Context context) {
+ context.addExtension(CustomPythonRuleRepository.class);
+ }
+
+}
diff --git a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheck.java b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheck.java
new file mode 100644
index 0000000000..e5d19099f2
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheck.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011-2024 SonarSource SA - mailto:info AT sonarsource DOT com
+ * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
+ */
+package org.sonar.samples.python.checks;
+
+import org.sonar.check.Priority;
+import org.sonar.check.Rule;
+import org.sonar.plugins.python.api.PythonSubscriptionCheck;
+import org.sonar.plugins.python.api.tree.ForStatement;
+import org.sonar.plugins.python.api.tree.Tree;
+
+@Rule(
+ key = CustomPythonSubscriptionCheck.RULE_KEY,
+ priority = Priority.MINOR,
+ name = "Python subscription visitor check",
+ description = "desc")
+public class CustomPythonSubscriptionCheck extends PythonSubscriptionCheck {
+
+ public static final String RULE_KEY = "subscription";
+
+ @Override
+ public void initialize(Context context) {
+ context.registerSyntaxNodeConsumer(Tree.Kind.FOR_STMT, ctx -> ctx.addIssue(((ForStatement) ctx.syntaxNode()).forKeyword(), "For statement."));
+ }
+}
diff --git a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonVisitorCheck.java b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonVisitorCheck.java
new file mode 100644
index 0000000000..cdfd46cd43
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/CustomPythonVisitorCheck.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2011-2024 SonarSource SA - mailto:info AT sonarsource DOT com
+ * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
+ */
+package org.sonar.samples.python.checks;
+
+import org.sonar.check.Priority;
+import org.sonar.check.Rule;
+import org.sonar.plugins.python.api.PythonVisitorCheck;
+import org.sonar.plugins.python.api.tree.FunctionDef;
+
+@Rule(
+ key = CustomPythonVisitorCheck.RULE_KEY,
+ priority = Priority.MINOR,
+ name = "Python visitor check",
+ description = "desc")
+public class CustomPythonVisitorCheck extends PythonVisitorCheck {
+
+ public static final String RULE_KEY = "visitor";
+
+ @Override
+ public void visitFunctionDef(FunctionDef pyFunctionDefTree) {
+ addIssue(pyFunctionDefTree.name(), "Function def.");
+ super.visitFunctionDef(pyFunctionDefTree);
+ }
+
+}
diff --git a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/package-info.java b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/package-info.java
new file mode 100644
index 0000000000..979ed078cf
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/checks/package-info.java
@@ -0,0 +1,9 @@
+/*
+ * Copyright (C) 2011-2024 SonarSource SA - mailto:info AT sonarsource DOT com
+ * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.samples.python.checks;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/package-info.java b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/package-info.java
new file mode 100644
index 0000000000..3cf9ca9ddf
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/main/java/org/sonar/samples/python/package-info.java
@@ -0,0 +1,9 @@
+/*
+ * Copyright (C) 2011-2024 SonarSource SA - mailto:info AT sonarsource DOT com
+ * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.samples.python;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/docs/python-custom-rule-examples/src/main/resources/license-header.txt b/docs/python-custom-rule-examples/src/main/resources/license-header.txt
new file mode 100644
index 0000000000..27a2c5c0c2
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/main/resources/license-header.txt
@@ -0,0 +1,2 @@
+Copyright (C) ${license.years} ${license.owner} - ${license.mailto}
+This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
\ No newline at end of file
diff --git a/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/subscription.html b/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/subscription.html
new file mode 100644
index 0000000000..bd3027f657
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/subscription.html
@@ -0,0 +1,9 @@
+raises an issue on a For statement.
+Noncompliant Code Example
+
+TO DO
+
+Compliant Solution
+
+TO DO
+
diff --git a/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/visitor.html b/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/visitor.html
new file mode 100644
index 0000000000..67572dce7f
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/main/resources/org/sonar/l10n/python/rules/python/visitor.html
@@ -0,0 +1,9 @@
+raises an issue on a For statement.
with a visitor on which you can control the visit.
+Noncompliant Code Example
+
+TO DO
+
+Compliant Solution
+
+TO DO
+
diff --git a/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRuleRepositoryTest.java b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRuleRepositoryTest.java
new file mode 100644
index 0000000000..86fb7f7c14
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRuleRepositoryTest.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2011-2024 SonarSource SA - mailto:info AT sonarsource DOT com
+ * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
+ */
+package org.sonar.samples.python;
+
+import org.junit.Test;
+import org.sonar.api.server.rule.RulesDefinition;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CustomPythonRuleRepositoryTest {
+
+ @Test
+ public void test_rule_repository() {
+ CustomPythonRuleRepository customPythonRuleRepository = new CustomPythonRuleRepository();
+ RulesDefinition.Context context = new RulesDefinition.Context();
+ customPythonRuleRepository.define(context);
+ assertThat(customPythonRuleRepository.repositoryKey()).isEqualTo("python-custom-rules");
+ assertThat(context.repositories()).hasSize(1).extracting("key").containsExactly(customPythonRuleRepository.repositoryKey());
+ assertThat(context.repositories().get(0).rules()).hasSize(2);
+ assertThat(customPythonRuleRepository.checkClasses()).hasSize(2);
+ }
+}
diff --git a/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRulesPluginTest.java b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRulesPluginTest.java
new file mode 100644
index 0000000000..565b627791
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/CustomPythonRulesPluginTest.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011-2024 SonarSource SA - mailto:info AT sonarsource DOT com
+ * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
+ */
+package org.sonar.samples.python;
+
+import org.junit.Test;
+import org.sonar.api.Plugin;
+import org.sonar.api.SonarEdition;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.internal.PluginContextImpl;
+import org.sonar.api.internal.SonarRuntimeImpl;
+import org.sonar.api.utils.Version;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CustomPythonRulesPluginTest {
+ @Test
+ public void test() {
+ SonarRuntime sonarRuntime = SonarRuntimeImpl.forSonarQube(Version.create(7, 9), SonarQubeSide.SCANNER, SonarEdition.DEVELOPER);
+ Plugin.Context context = new PluginContextImpl.Builder().setSonarRuntime(sonarRuntime).build();
+ new CustomPythonRulesPlugin().define(context);
+ assertThat(context.getExtensions()).hasSize(1);
+ }
+}
diff --git a/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheckTest.java b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheckTest.java
new file mode 100644
index 0000000000..85daa7d314
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/checks/CustomPythonSubscriptionCheckTest.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2011-2024 SonarSource SA - mailto:info AT sonarsource DOT com
+ * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
+ */
+package org.sonar.samples.python.checks;
+
+import org.junit.Test;
+import org.sonar.python.checks.utils.PythonCheckVerifier;
+import org.sonar.samples.python.checks.CustomPythonSubscriptionCheck;
+
+public class CustomPythonSubscriptionCheckTest {
+ @Test
+ public void test() {
+ PythonCheckVerifier.verify("src/test/resources/checks/customPythonSubscriptionCheck.py", new CustomPythonSubscriptionCheck());
+ }
+}
diff --git a/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/checks/CustomPythonVisitorCheckTest.java b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/checks/CustomPythonVisitorCheckTest.java
new file mode 100644
index 0000000000..ebc926d5a2
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/test/java/org/sonar/samples/python/checks/CustomPythonVisitorCheckTest.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2011-2024 SonarSource SA - mailto:info AT sonarsource DOT com
+ * This code is released under [MIT No Attribution](https://opensource.org/licenses/MIT-0) license.
+ */
+package org.sonar.samples.python.checks;
+
+
+import org.junit.Test;
+import org.sonar.python.checks.utils.PythonCheckVerifier;
+import org.sonar.samples.python.checks.CustomPythonVisitorCheck;
+
+public class CustomPythonVisitorCheckTest {
+ @Test
+ public void test() {
+ PythonCheckVerifier.verify("src/test/resources/checks/customPythonVisitorCheck.py", new CustomPythonVisitorCheck());
+ }
+}
diff --git a/docs/python-custom-rule-examples/src/test/resources/checks/customPythonSubscriptionCheck.py b/docs/python-custom-rule-examples/src/test/resources/checks/customPythonSubscriptionCheck.py
new file mode 100644
index 0000000000..0dbd62c4f8
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/test/resources/checks/customPythonSubscriptionCheck.py
@@ -0,0 +1,4 @@
+class A:
+ for i in foo: # Noncompliant {{For statement.}}
+# ^^^
+ pass
diff --git a/docs/python-custom-rule-examples/src/test/resources/checks/customPythonVisitorCheck.py b/docs/python-custom-rule-examples/src/test/resources/checks/customPythonVisitorCheck.py
new file mode 100644
index 0000000000..aeda8ea08d
--- /dev/null
+++ b/docs/python-custom-rule-examples/src/test/resources/checks/customPythonVisitorCheck.py
@@ -0,0 +1,4 @@
+class A:
+ def fun(): # Noncompliant {{Function def.}}
+# ^^^
+ pass
diff --git a/pom.xml b/pom.xml
index 7efb4bb4af..e3c7f611ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,6 +51,7 @@
sonar-python-plugin
its
python-checks-testkit
+ docs