sonicop 26.8.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/CONFORMANCE.md +109 -0
  3. data/Cargo.lock +975 -0
  4. data/Cargo.toml +40 -0
  5. data/LICENSE +21 -0
  6. data/NOTICE +7 -0
  7. data/README.ja.md +161 -0
  8. data/README.md +173 -0
  9. data/config/default.yml +6305 -0
  10. data/exe/sonicop +7 -0
  11. data/ext/sonicop/extconf.rb +38 -0
  12. data/lib/sonicop/runner.rb +67 -0
  13. data/lib/sonicop/version.rb +6 -0
  14. data/lib/sonicop.rb +8 -0
  15. data/licenses/RUBOCOP.txt +20 -0
  16. data/licenses/TREE_SITTER_RUBY.txt +21 -0
  17. data/src/cli.rs +940 -0
  18. data/src/config/inheritance.rs +332 -0
  19. data/src/config/loader.rs +117 -0
  20. data/src/config/mod.rs +461 -0
  21. data/src/config/paths.rs +436 -0
  22. data/src/config/plugin.rs +247 -0
  23. data/src/config/store.rs +141 -0
  24. data/src/cop_name.rs +71 -0
  25. data/src/diagnostic.rs +205 -0
  26. data/src/directives.rs +305 -0
  27. data/src/engine.rs +734 -0
  28. data/src/formatter.rs +684 -0
  29. data/src/lib.rs +42 -0
  30. data/src/main.rs +3 -0
  31. data/src/ruby_version.rs +393 -0
  32. data/src/rules/layout/empty_line_after_magic_comment.rs +49 -0
  33. data/src/rules/layout/end_of_line.rs +35 -0
  34. data/src/rules/layout/line_length.rs +167 -0
  35. data/src/rules/layout/mod.rs +13 -0
  36. data/src/rules/layout/space_after_comma.rs +32 -0
  37. data/src/rules/layout/space_around_operators.rs +104 -0
  38. data/src/rules/layout/space_inside_parens.rs +100 -0
  39. data/src/rules/layout/support.rs +23 -0
  40. data/src/rules/layout/trailing_empty_lines.rs +38 -0
  41. data/src/rules/layout/trailing_whitespace.rs +26 -0
  42. data/src/rules/lint/duplicate_methods.rs +72 -0
  43. data/src/rules/lint/mod.rs +7 -0
  44. data/src/rules/lint/syntax.rs +200 -0
  45. data/src/rules/lint/unused_block_argument.rs +74 -0
  46. data/src/rules/lint/useless_assignment.rs +104 -0
  47. data/src/rules/metrics/block_length.rs +39 -0
  48. data/src/rules/metrics/class_length.rs +34 -0
  49. data/src/rules/metrics/method_length.rs +10 -0
  50. data/src/rules/metrics/mod.rs +10 -0
  51. data/src/rules/metrics/module_length.rs +17 -0
  52. data/src/rules/metrics/parameter_lists.rs +21 -0
  53. data/src/rules/metrics/support.rs +105 -0
  54. data/src/rules/mod.rs +380 -0
  55. data/src/rules/naming/ascii_identifiers.rs +17 -0
  56. data/src/rules/naming/constant_name.rs +49 -0
  57. data/src/rules/naming/method_name.rs +51 -0
  58. data/src/rules/naming/mod.rs +9 -0
  59. data/src/rules/naming/support.rs +22 -0
  60. data/src/rules/naming/variable_name.rs +50 -0
  61. data/src/rules/security/eval.rs +170 -0
  62. data/src/rules/security/mod.rs +6 -0
  63. data/src/rules/style/frozen_string_literal_comment.rs +56 -0
  64. data/src/rules/style/hash_syntax.rs +63 -0
  65. data/src/rules/style/mod.rs +9 -0
  66. data/src/rules/style/numeric_literals.rs +59 -0
  67. data/src/rules/style/redundant_return.rs +143 -0
  68. data/src/rules/style/semicolon.rs +48 -0
  69. data/src/rules/style/string_literals.rs +74 -0
  70. data/src/rules/support.rs +31 -0
  71. data/src/source.rs +118 -0
  72. metadata +117 -0
@@ -0,0 +1,74 @@
1
+ use std::collections::HashSet;
2
+
3
+ use tree_sitter::Node;
4
+
5
+ use crate::diagnostic::{Edit, Offense};
6
+ use crate::rules::{RuleContext, first_identifier, walk_named};
7
+
8
+ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
9
+ let ignore_empty: bool = context.setting("IgnoreEmptyBlocks").unwrap_or(true);
10
+ for node in context.nodes_of_any(&["block", "do_block"]) {
11
+ let (Some(parameters), Some(body)) = (
12
+ node.child_by_field_name("parameters"),
13
+ node.child_by_field_name("body"),
14
+ ) else {
15
+ continue;
16
+ };
17
+ if ignore_empty && context.source.node_text(body).trim().is_empty() {
18
+ continue;
19
+ }
20
+
21
+ let mut parameter_nodes = Vec::new();
22
+ let mut seen = HashSet::new();
23
+ let mut cursor = parameters.walk();
24
+ for parameter in parameters.named_children(&mut cursor) {
25
+ if parameter.kind() == "identifier" {
26
+ if seen.insert(parameter.start_byte()) {
27
+ parameter_nodes.push(parameter);
28
+ }
29
+ } else if let Some(identifier) = first_identifier(parameter)
30
+ && seen.insert(identifier.start_byte())
31
+ {
32
+ parameter_nodes.push(identifier);
33
+ }
34
+ }
35
+
36
+ for parameter in parameter_nodes {
37
+ let name = context.source.node_text(parameter);
38
+ if name.starts_with('_') || identifier_used(body, name, parameter, context) {
39
+ continue;
40
+ }
41
+ offenses.push(
42
+ context
43
+ .offense(
44
+ format!("Unused block argument - `{name}`. If it's necessary, use `_` or `_name` as an argument name."),
45
+ parameter.byte_range(),
46
+ )
47
+ .corrected_by(Edit {
48
+ start: parameter.start_byte(),
49
+ end: parameter.start_byte(),
50
+ replacement: "_".to_owned(),
51
+ safe: true,
52
+ }),
53
+ );
54
+ }
55
+ }
56
+ }
57
+
58
+ fn identifier_used(
59
+ body: Node<'_>,
60
+ name: &str,
61
+ definition: Node<'_>,
62
+ context: &RuleContext<'_>,
63
+ ) -> bool {
64
+ let mut used = false;
65
+ walk_named(body, &mut |candidate| {
66
+ if candidate.kind() == "identifier"
67
+ && candidate.byte_range() != definition.byte_range()
68
+ && context.source.node_text(candidate) == name
69
+ {
70
+ used = true;
71
+ }
72
+ });
73
+ used
74
+ }
@@ -0,0 +1,104 @@
1
+ use tree_sitter::Node;
2
+
3
+ use crate::diagnostic::{Edit, Offense};
4
+ use crate::rules::{RuleContext, walk_named};
5
+
6
+ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
7
+ let root = context.root_node();
8
+ for assignment in context.nodes_of("assignment") {
9
+ let Some(left) = assignment.child_by_field_name("left") else {
10
+ continue;
11
+ };
12
+ if left.kind() != "identifier" {
13
+ continue;
14
+ }
15
+ let name = context.source.node_text(left);
16
+ if name.starts_with('_') {
17
+ continue;
18
+ }
19
+ if assignment.parent().is_some_and(|parent| {
20
+ parent.kind() == "assignment"
21
+ && parent
22
+ .child_by_field_name("right")
23
+ .is_some_and(|right| right.byte_range() == assignment.byte_range())
24
+ }) {
25
+ continue;
26
+ }
27
+ let scope = enclosing_scope(assignment).unwrap_or(root);
28
+ if has_other_read(scope, assignment, name, context) {
29
+ continue;
30
+ }
31
+ offenses.push(
32
+ context
33
+ .offense(
34
+ format!("Useless assignment to variable - `{name}`."),
35
+ left.byte_range(),
36
+ )
37
+ .corrected_by(Edit {
38
+ start: assignment.start_byte(),
39
+ end: assignment
40
+ .child_by_field_name("right")
41
+ .map_or(left.end_byte(), |right| right.start_byte()),
42
+ replacement: String::new(),
43
+ safe: true,
44
+ }),
45
+ );
46
+ }
47
+ }
48
+
49
+ fn enclosing_scope(mut node: Node<'_>) -> Option<Node<'_>> {
50
+ while let Some(parent) = node.parent() {
51
+ if matches!(parent.kind(), "method" | "singleton_method") {
52
+ return Some(parent);
53
+ }
54
+ node = parent;
55
+ }
56
+ None
57
+ }
58
+
59
+ fn has_other_read(
60
+ scope: Node<'_>,
61
+ assignment: Node<'_>,
62
+ name: &str,
63
+ context: &RuleContext<'_>,
64
+ ) -> bool {
65
+ let mut found = false;
66
+ walk_named(scope, &mut |candidate| {
67
+ if found {
68
+ return;
69
+ }
70
+ if candidate.kind() == "pair"
71
+ && context.source.node_text(candidate).trim() == format!("{name}:")
72
+ {
73
+ found = true;
74
+ return;
75
+ }
76
+ if candidate.kind() == "call"
77
+ && candidate
78
+ .child_by_field_name("method")
79
+ .is_some_and(|method| context.source.node_text(method) == "binding")
80
+ {
81
+ found = true;
82
+ return;
83
+ }
84
+ if candidate.kind() == "identifier" && context.source.node_text(candidate) == "binding" {
85
+ found = true;
86
+ return;
87
+ }
88
+ if candidate.kind() != "identifier" || context.source.node_text(candidate) != name {
89
+ return;
90
+ }
91
+ let is_this_assignment = candidate.byte_range()
92
+ == assignment
93
+ .child_by_field_name("left")
94
+ .map_or(assignment.byte_range(), |left| left.byte_range());
95
+ let is_write = candidate.parent().is_some_and(|parent| {
96
+ parent.kind() == "assignment"
97
+ && parent
98
+ .child_by_field_name("left")
99
+ .is_some_and(|left| left.byte_range() == candidate.byte_range())
100
+ });
101
+ found = !is_this_assignment && !is_write;
102
+ });
103
+ found
104
+ }
@@ -0,0 +1,39 @@
1
+ use tree_sitter::Node;
2
+
3
+ use super::support::{LengthTarget, report_length};
4
+ use crate::diagnostic::Offense;
5
+ use crate::rules::RuleContext;
6
+
7
+ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
8
+ let max: usize = context.setting("Max").unwrap_or(25);
9
+ let allowed: Vec<String> = context.setting("AllowedMethods").unwrap_or_default();
10
+ for node in context.nodes_of_any(&["block", "do_block"]) {
11
+ if block_method_allowed(node, context, &allowed) {
12
+ continue;
13
+ }
14
+ report_length(context, offenses, node, max, "Block", LengthTarget::Block);
15
+ }
16
+ }
17
+
18
+ /// `Class.new`/`Struct.new` bodies are class definitions in disguise, which RuboCop measures with
19
+ /// `Metrics/ClassLength` instead.
20
+ fn block_method_allowed(node: Node<'_>, context: &RuleContext<'_>, allowed: &[String]) -> bool {
21
+ let Some(call) = node.parent().filter(|parent| parent.kind() == "call") else {
22
+ return false;
23
+ };
24
+ let Some(method) = call.child_by_field_name("method") else {
25
+ return false;
26
+ };
27
+ if context.source.node_text(method) == "new"
28
+ && call
29
+ .child_by_field_name("receiver")
30
+ .is_some_and(|receiver| {
31
+ matches!(context.source.node_text(receiver), "Class" | "Struct")
32
+ })
33
+ {
34
+ return true;
35
+ }
36
+ allowed
37
+ .iter()
38
+ .any(|name| name == context.source.node_text(method))
39
+ }
@@ -0,0 +1,34 @@
1
+ use tree_sitter::Node;
2
+
3
+ use super::support::{LengthTarget, report_length};
4
+ use crate::diagnostic::Offense;
5
+ use crate::rules::RuleContext;
6
+
7
+ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
8
+ let max: usize = context.setting("Max").unwrap_or(100);
9
+ for node in context.nodes_of_any(&["class", "singleton_class"]) {
10
+ // A `class << self` inside a class body is part of that class's length rather than a
11
+ // class of its own.
12
+ if node.kind() == "singleton_class" && has_class_ancestor(node) {
13
+ continue;
14
+ }
15
+ report_length(
16
+ context,
17
+ offenses,
18
+ node,
19
+ max,
20
+ "Class",
21
+ LengthTarget::Classlike,
22
+ );
23
+ }
24
+ }
25
+
26
+ fn has_class_ancestor(mut node: Node<'_>) -> bool {
27
+ while let Some(parent) = node.parent() {
28
+ if parent.kind() == "class" {
29
+ return true;
30
+ }
31
+ node = parent;
32
+ }
33
+ false
34
+ }
@@ -0,0 +1,10 @@
1
+ use super::support::{LengthTarget, report_length};
2
+ use crate::diagnostic::Offense;
3
+ use crate::rules::RuleContext;
4
+
5
+ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
6
+ let max: usize = context.setting("Max").unwrap_or(10);
7
+ for node in context.nodes_of_any(&["method", "singleton_method"]) {
8
+ report_length(context, offenses, node, max, "Method", LengthTarget::Method);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ department_rules! {
2
+ "Metrics";
3
+ block_length => ("BlockLength", Convention),
4
+ class_length => ("ClassLength", Convention),
5
+ method_length => ("MethodLength", Convention),
6
+ module_length => ("ModuleLength", Convention),
7
+ parameter_lists => ("ParameterLists", Convention),
8
+ }
9
+
10
+ mod support;
@@ -0,0 +1,17 @@
1
+ use super::support::{LengthTarget, report_length};
2
+ use crate::diagnostic::Offense;
3
+ use crate::rules::RuleContext;
4
+
5
+ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
6
+ let max: usize = context.setting("Max").unwrap_or(100);
7
+ for node in context.nodes_of("module") {
8
+ report_length(
9
+ context,
10
+ offenses,
11
+ node,
12
+ max,
13
+ "Module",
14
+ LengthTarget::Classlike,
15
+ );
16
+ }
17
+ }
@@ -0,0 +1,21 @@
1
+ use crate::diagnostic::Offense;
2
+ use crate::rules::RuleContext;
3
+
4
+ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
5
+ let max: usize = context.setting("Max").unwrap_or(5);
6
+ let count_keywords: bool = context.setting("CountKeywordArgs").unwrap_or(true);
7
+ for node in context.nodes_of("method_parameters") {
8
+ let mut cursor = node.walk();
9
+ let count = node
10
+ .named_children(&mut cursor)
11
+ .filter(|parameter| count_keywords || parameter.kind() != "keyword_parameter")
12
+ .count();
13
+ if count <= max {
14
+ continue;
15
+ }
16
+ offenses.push(context.offense(
17
+ format!("Avoid parameter lists longer than {max} parameters. [{count}/{max}]"),
18
+ node.byte_range(),
19
+ ));
20
+ }
21
+ }
@@ -0,0 +1,105 @@
1
+ //! Line counting shared by the length cops.
2
+
3
+ use std::collections::HashSet;
4
+
5
+ use tree_sitter::Node;
6
+
7
+ use crate::diagnostic::Offense;
8
+ use crate::rules::{RuleContext, walk_named};
9
+
10
+ /// What kind of construct a length cop measures. The three differ in how the body is counted and
11
+ /// where the offense is reported, so naming the kind keeps those differences in one place instead
12
+ /// of spreading cop-name comparisons through the counting code.
13
+ #[derive(Clone, Copy, Eq, PartialEq)]
14
+ pub(super) enum LengthTarget {
15
+ /// A method, counted over its body.
16
+ Method,
17
+ /// A class or module, counted over its interior with nested classes and modules removed.
18
+ Classlike,
19
+ /// A block, reported against the call that owns it.
20
+ Block,
21
+ }
22
+
23
+ /// Reports `node` when it holds more than `max` lines of code, in the shape RuboCop's length cops
24
+ /// use: `Method has too many lines. [12/10]`.
25
+ pub(super) fn report_length(
26
+ context: &RuleContext<'_>,
27
+ offenses: &mut Vec<Offense>,
28
+ node: Node<'_>,
29
+ max: usize,
30
+ label: &str,
31
+ target: LengthTarget,
32
+ ) {
33
+ let count_comments: bool = context.setting("CountComments").unwrap_or(false);
34
+ let Some(body) = node.child_by_field_name("body") else {
35
+ return;
36
+ };
37
+ let mut length = if target == LengthTarget::Classlike {
38
+ classlike_code_line_count(node, context, count_comments)
39
+ } else {
40
+ code_line_count(body, context, count_comments)
41
+ };
42
+ // A block whose whole body is one heredoc spends a line on the heredoc opener, which RuboCop
43
+ // attributes to the enclosing statement rather than to the block.
44
+ if target == LengthTarget::Block
45
+ && body.named_child_count() == 1
46
+ && context.heredoc_count(body.byte_range()) > 0
47
+ {
48
+ length = length.saturating_sub(1);
49
+ }
50
+ if length <= max {
51
+ return;
52
+ }
53
+ let location = if target == LengthTarget::Block {
54
+ node.parent()
55
+ .filter(|parent| parent.kind() == "call")
56
+ .unwrap_or(node)
57
+ } else {
58
+ node
59
+ };
60
+ offenses.push(context.offense(
61
+ format!("{label} has too many lines. [{length}/{max}]"),
62
+ location.byte_range(),
63
+ ));
64
+ }
65
+
66
+ fn classlike_code_line_count(
67
+ node: Node<'_>,
68
+ context: &RuleContext<'_>,
69
+ count_comments: bool,
70
+ ) -> usize {
71
+ let mut excluded_lines = HashSet::new();
72
+ walk_named(node, &mut |descendant| {
73
+ if descendant == node || !matches!(descendant.kind(), "class" | "module") {
74
+ return;
75
+ }
76
+ let first = descendant.start_position().row + 1;
77
+ let last = descendant.end_position().row + 1;
78
+ excluded_lines.extend(first..=last);
79
+ });
80
+
81
+ // RuboCop's ProcessedSource is indexed from zero after constructing the
82
+ // one-based interior line range. Preserve that observable offset exactly.
83
+ let start = node.start_position().row + 2;
84
+ let end = node.end_position().row;
85
+ (start..=end)
86
+ .filter(|line| {
87
+ if excluded_lines.contains(line) {
88
+ return false;
89
+ }
90
+ let text = context.source.line(*line + 1).trim();
91
+ !text.is_empty() && (count_comments || !text.starts_with('#'))
92
+ })
93
+ .count()
94
+ }
95
+
96
+ fn code_line_count(node: Node<'_>, context: &RuleContext<'_>, count_comments: bool) -> usize {
97
+ let start = node.start_position().row + 1;
98
+ let end = node.end_position().row + 1;
99
+ (start..=end)
100
+ .filter(|line| {
101
+ let text = context.source.line(*line).trim();
102
+ !text.is_empty() && (count_comments || !text.starts_with('#'))
103
+ })
104
+ .count()
105
+ }