aidp 0.5.0 → 0.7.0

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.
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Analysis
5
+ module Seams
6
+ # I/O and OS integration patterns
7
+ IO_PATTERNS = [
8
+ /^File\./, /^IO\./, /^Kernel\.system$/, /^Open3\./, /^Net::HTTP/,
9
+ /Socket|TCPSocket|UDPSocket/, /^Dir\./, /^ENV/, /^ARGV/,
10
+ /^STDIN|^STDOUT|^STDERR/, /^Process\./, /^Thread\./, /^Timeout\./
11
+ ].freeze
12
+
13
+ # Database and external service patterns
14
+ EXTERNAL_SERVICE_PATTERNS = [
15
+ /ActiveRecord|Sequel|DataMapper/, /Redis|Memcached/, /Elasticsearch/,
16
+ /AWS::|Google::|Azure::/, /HTTParty|Faraday|Net::HTTP/,
17
+ /Sidekiq|Resque|DelayedJob/, /ActionMailer|Mail/
18
+ ].freeze
19
+
20
+ # Global and singleton patterns
21
+ GLOBAL_PATTERNS = [
22
+ /^\$[a-zA-Z_]/, /^@@[a-zA-Z_]/, /^::[A-Z]/, /^Kernel\./,
23
+ /include Singleton/, /extend Singleton/, /@singleton/
24
+ ].freeze
25
+
26
+ # Constructor with work indicators
27
+ CONSTRUCTOR_WORK_INDICATORS = [
28
+ /File\./, /IO\./, /Net::/, /HTTP/, /Database/, /Redis/,
29
+ /system\(/, /exec\(/, /spawn\(/, /fork\(/
30
+ ].freeze
31
+
32
+ def self.io_call?(receiver, method)
33
+ fq = [receiver, method].compact.join(".")
34
+ IO_PATTERNS.any? { |pattern| fq.match?(pattern) }
35
+ end
36
+
37
+ def self.external_service_call?(receiver, method)
38
+ fq = [receiver, method].compact.join(".")
39
+ EXTERNAL_SERVICE_PATTERNS.any? { |pattern| fq.match?(pattern) }
40
+ end
41
+
42
+ def self.global_or_singleton?(ast_node_texts)
43
+ ast_node_texts.any? { |text| GLOBAL_PATTERNS.any? { |pattern| text.match?(pattern) } }
44
+ end
45
+
46
+ def self.constructor_with_work?(node, metrics)
47
+ return false unless node[:type] == "method" && node[:name] == "initialize"
48
+
49
+ # Check if constructor has significant work based on metrics
50
+ metrics[:branch_count].to_i > 2 ||
51
+ metrics[:fan_out].to_i > 3 ||
52
+ metrics[:lines].to_i > 10
53
+ end
54
+
55
+ def self.detect_seams_in_ast(ast_nodes, file_path)
56
+ seams = []
57
+
58
+ ast_nodes.each do |node|
59
+ case node[:type]
60
+ when "method"
61
+ seams.concat(detect_method_seams(node, file_path))
62
+ when "class", "module"
63
+ seams.concat(detect_class_module_seams(node, file_path))
64
+ end
65
+ end
66
+
67
+ seams
68
+ end
69
+
70
+ def self.detect_method_seams(method_node, file_path)
71
+ seams = []
72
+
73
+ # Check for I/O calls in method body
74
+ if (io_calls = extract_io_calls(method_node))
75
+ io_calls.each do |call|
76
+ seams << {
77
+ kind: "io_integration",
78
+ file: file_path,
79
+ line: call[:line],
80
+ symbol_id: "#{file_path}:#{method_node[:line]}:#{method_node[:name]}",
81
+ detail: {
82
+ call: call[:call],
83
+ receiver: call[:receiver],
84
+ method: call[:method]
85
+ },
86
+ suggestion: "Consider extracting I/O operations to a separate service class"
87
+ }
88
+ end
89
+ end
90
+
91
+ # Check for external service calls
92
+ if (service_calls = extract_external_service_calls(method_node))
93
+ service_calls.each do |call|
94
+ seams << {
95
+ kind: "external_service",
96
+ file: file_path,
97
+ line: call[:line],
98
+ symbol_id: "#{file_path}:#{method_node[:line]}:#{method_node[:name]}",
99
+ detail: {
100
+ call: call[:call],
101
+ service: call[:service]
102
+ },
103
+ suggestion: "Consider using dependency injection for external service calls"
104
+ }
105
+ end
106
+ end
107
+
108
+ # Check for global/singleton usage
109
+ if (global_usage = extract_global_usage(method_node))
110
+ global_usage.each do |usage|
111
+ seams << {
112
+ kind: "global_singleton",
113
+ file: file_path,
114
+ line: usage[:line],
115
+ symbol_id: "#{file_path}:#{method_node[:line]}:#{method_node[:name]}",
116
+ detail: {
117
+ usage: usage[:usage],
118
+ type: usage[:type]
119
+ },
120
+ suggestion: "Consider passing global state as parameters or using dependency injection"
121
+ }
122
+ end
123
+ end
124
+
125
+ seams
126
+ end
127
+
128
+ def self.detect_class_module_seams(class_node, file_path)
129
+ seams = []
130
+
131
+ # Check for singleton pattern
132
+ if class_node[:content]&.include?("include Singleton")
133
+ seams << {
134
+ kind: "singleton_pattern",
135
+ file: file_path,
136
+ line: class_node[:line],
137
+ symbol_id: "#{file_path}:#{class_node[:line]}:#{class_node[:name]}",
138
+ detail: {
139
+ pattern: "Singleton",
140
+ class_name: class_node[:name]
141
+ },
142
+ suggestion: "Consider using dependency injection instead of singleton pattern"
143
+ }
144
+ end
145
+
146
+ # Check for global state in class/module
147
+ if (global_state = extract_global_state(class_node))
148
+ global_state.each do |state|
149
+ seams << {
150
+ kind: "global_state",
151
+ file: file_path,
152
+ line: state[:line],
153
+ symbol_id: "#{file_path}:#{class_node[:line]}:#{class_node[:name]}",
154
+ detail: {
155
+ state: state[:state],
156
+ type: state[:type]
157
+ },
158
+ suggestion: "Consider encapsulating global state or using configuration objects"
159
+ }
160
+ end
161
+ end
162
+
163
+ seams
164
+ end
165
+
166
+ private_class_method def self.extract_io_calls(_method_node)
167
+ # This would extract I/O calls from the method's AST
168
+ # For now, return mock data
169
+ []
170
+ end
171
+
172
+ private_class_method def self.extract_external_service_calls(_method_node)
173
+ # This would extract external service calls from the method's AST
174
+ []
175
+ end
176
+
177
+ private_class_method def self.extract_global_usage(_method_node)
178
+ # This would extract global variable usage from the method's AST
179
+ []
180
+ end
181
+
182
+ private_class_method def self.extract_global_state(_class_node)
183
+ # This would extract global state from the class/module AST
184
+ []
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,493 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tree_sitter"
4
+ require "fileutils"
5
+
6
+ module Aidp
7
+ module Analysis
8
+ class TreeSitterGrammarLoader
9
+ # Default grammar configurations
10
+ GRAMMAR_CONFIGS = {
11
+ "ruby" => {
12
+ name: "tree-sitter-ruby",
13
+ version: "0.20.0",
14
+ source: "https://github.com/tree-sitter/tree-sitter-ruby",
15
+ file_patterns: ["**/*.rb"],
16
+ node_types: {
17
+ class: "class",
18
+ module: "module",
19
+ method: "method",
20
+ call: "call",
21
+ require: "call",
22
+ require_relative: "call"
23
+ }
24
+ },
25
+ "javascript" => {
26
+ name: "tree-sitter-javascript",
27
+ version: "0.20.0",
28
+ source: "https://github.com/tree-sitter/tree-sitter-javascript",
29
+ file_patterns: ["**/*.js", "**/*.jsx"],
30
+ node_types: {
31
+ class: "class_declaration",
32
+ function: "function_declaration",
33
+ call: "call_expression",
34
+ import: "import_statement"
35
+ }
36
+ },
37
+ "typescript" => {
38
+ name: "tree-sitter-typescript",
39
+ version: "0.20.0",
40
+ source: "https://github.com/tree-sitter/tree-sitter-typescript",
41
+ file_patterns: ["**/*.ts", "**/*.tsx"],
42
+ node_types: {
43
+ class: "class_declaration",
44
+ function: "function_declaration",
45
+ call: "call_expression",
46
+ import: "import_statement"
47
+ }
48
+ },
49
+ "python" => {
50
+ name: "tree-sitter-python",
51
+ version: "0.20.0",
52
+ source: "https://github.com/tree-sitter/tree-sitter-python",
53
+ file_patterns: ["**/*.py"],
54
+ node_types: {
55
+ class: "class_definition",
56
+ function: "function_definition",
57
+ call: "call",
58
+ import: "import_statement"
59
+ }
60
+ }
61
+ }.freeze
62
+
63
+ def initialize(project_dir = Dir.pwd)
64
+ @project_dir = project_dir
65
+ @grammars_dir = File.join(project_dir, ".aidp", "grammars")
66
+ @loaded_grammars = {}
67
+ end
68
+
69
+ # Load grammar for a specific language
70
+ def load_grammar(language)
71
+ return @loaded_grammars[language] if @loaded_grammars[language]
72
+
73
+ config = GRAMMAR_CONFIGS[language]
74
+ raise "Unsupported language: #{language}" unless config
75
+
76
+ ensure_grammar_available(language, config)
77
+ @loaded_grammars[language] = create_parser(language, config)
78
+ end
79
+
80
+ # Get supported languages
81
+ def supported_languages
82
+ GRAMMAR_CONFIGS.keys
83
+ end
84
+
85
+ # Get file patterns for a language
86
+ def file_patterns_for_language(language)
87
+ config = GRAMMAR_CONFIGS[language]
88
+ return [] unless config
89
+
90
+ config[:file_patterns]
91
+ end
92
+
93
+ # Get node types for a language
94
+ def node_types_for_language(language)
95
+ config = GRAMMAR_CONFIGS[language]
96
+ return {} unless config
97
+
98
+ config[:node_types]
99
+ end
100
+
101
+ private
102
+
103
+ def ensure_grammar_available(language, config)
104
+ grammar_path = File.join(@grammars_dir, language)
105
+
106
+ unless File.exist?(grammar_path)
107
+ puts "Installing Tree-sitter grammar for #{language}..."
108
+ install_grammar(language, config)
109
+ end
110
+ end
111
+
112
+ def install_grammar(language, config)
113
+ FileUtils.mkdir_p(@grammars_dir)
114
+
115
+ # For now, we'll use the system-installed grammars
116
+ # In a production setup, you might want to download and compile grammars
117
+ grammar_path = File.join(@grammars_dir, language)
118
+
119
+ # Create a placeholder file to indicate the grammar is "installed"
120
+ # The actual grammar loading will be handled by tree_sitter
121
+ FileUtils.mkdir_p(grammar_path)
122
+ require "json"
123
+ File.write(File.join(grammar_path, "grammar.json"), JSON.generate(config))
124
+
125
+ puts "Grammar for #{language} marked as available"
126
+ end
127
+
128
+ def create_parser(language, config)
129
+ # Create a Tree-sitter parser for the language
130
+ # This is a simplified version - in practice you'd need to handle
131
+ # the actual grammar loading from the ruby_tree_sitter gem
132
+
133
+ {
134
+ language: language,
135
+ config: config,
136
+ parser: create_tree_sitter_parser(language)
137
+ }
138
+ end
139
+
140
+ def create_tree_sitter_parser(language)
141
+ create_real_parser(language)
142
+ end
143
+
144
+ def create_real_parser(language)
145
+ parser = TreeSitter::Parser.new
146
+ language_obj = TreeSitter.lang(language)
147
+ parser.language = language_obj
148
+
149
+ {
150
+ parse: ->(source_code) { parse_with_tree_sitter(parser, source_code) },
151
+ language: language,
152
+ real: true
153
+ }
154
+ rescue TreeSitter::ParserNotFoundError => e
155
+ puts "Warning: Tree-sitter parser not found for #{language}: #{e.message}"
156
+ create_mock_parser(language)
157
+ end
158
+
159
+ def create_mock_parser(language)
160
+ case language
161
+ when "ruby"
162
+ create_ruby_parser
163
+ when "javascript"
164
+ create_javascript_parser
165
+ when "typescript"
166
+ create_typescript_parser
167
+ when "python"
168
+ create_python_parser
169
+ else
170
+ raise "Unsupported language: #{language}"
171
+ end
172
+ end
173
+
174
+ def parse_with_tree_sitter(parser, source_code)
175
+ tree = parser.parse_string(nil, source_code)
176
+ root = tree.root_node
177
+
178
+ # Convert Tree-sitter AST to our internal format
179
+ convert_tree_sitter_ast(root, source_code)
180
+ end
181
+
182
+ def convert_tree_sitter_ast(node, source_code)
183
+ children = []
184
+
185
+ node.each do |child|
186
+ child_data = {
187
+ type: child.type,
188
+ name: extract_node_name(child, source_code),
189
+ line: child.start_point.row + 1,
190
+ start_column: child.start_point.column,
191
+ end_column: child.end_point.column
192
+ }
193
+
194
+ # Recursively process child nodes
195
+ if child.child_count > 0
196
+ child_data[:children] = convert_tree_sitter_ast(child, source_code)
197
+ end
198
+
199
+ children << child_data
200
+ end
201
+
202
+ {
203
+ type: node.type,
204
+ children: children
205
+ }
206
+ end
207
+
208
+ def extract_node_name(node, source_code)
209
+ # Extract meaningful names from nodes
210
+ case node.type.to_s
211
+ when "class", "module"
212
+ # Look for the class/module name in the source, handling nested constants
213
+ lines = source_code.lines
214
+ line_content = lines[node.start_point.row] || ""
215
+ # Handle patterns like: class Foo, class Foo::Bar, module A::B::C
216
+ if (match = line_content.match(/(?:class|module)\s+((?:\w+::)*\w+)/))
217
+ match[1]
218
+ else
219
+ node.type.to_s
220
+ end
221
+ when "method"
222
+ # Look for method name, handling various definition styles
223
+ lines = source_code.lines
224
+ line_content = lines[node.start_point.row] || ""
225
+ # Handle: def foo, def foo(args), def foo=(value), def []=(key, value)
226
+ if (match = line_content.match(/def\s+([\w\[\]=!?]+)/))
227
+ match[1]
228
+ else
229
+ node.type.to_s
230
+ end
231
+ when "singleton_method"
232
+ # Look for singleton method name, handling class methods
233
+ lines = source_code.lines
234
+ line_content = lines[node.start_point.row] || ""
235
+ # Handle: def self.foo, def ClassName.foo, def obj.method_name
236
+ if (match = line_content.match(/def\s+(?:self|[\w:]+)\.([\w\[\]=!?]+)/))
237
+ match[1]
238
+ else
239
+ node.type.to_s
240
+ end
241
+ when "constant"
242
+ # Extract constant names
243
+ lines = source_code.lines
244
+ line_content = lines[node.start_point.row] || ""
245
+ # Handle: CONSTANT = value, A::B::CONSTANT = value
246
+ if (match = line_content.match(/((?:\w+::)*[A-Z_][A-Z0-9_]*)\s*=/))
247
+ match[1]
248
+ else
249
+ node.type.to_s
250
+ end
251
+ else
252
+ node.type.to_s
253
+ end
254
+ end
255
+
256
+ def create_ruby_parser
257
+ # Mock Ruby parser - fallback when Tree-sitter is not available
258
+ {
259
+ parse: ->(source_code) { parse_ruby_source(source_code) },
260
+ language: "ruby",
261
+ real: false
262
+ }
263
+ end
264
+
265
+ def create_javascript_parser
266
+ {
267
+ parse: ->(source_code) { parse_javascript_source(source_code) },
268
+ language: "javascript",
269
+ real: false
270
+ }
271
+ end
272
+
273
+ def create_typescript_parser
274
+ {
275
+ parse: ->(source_code) { parse_typescript_source(source_code) },
276
+ language: "typescript",
277
+ real: false
278
+ }
279
+ end
280
+
281
+ def create_python_parser
282
+ {
283
+ parse: ->(source_code) { parse_python_source(source_code) },
284
+ language: "python",
285
+ real: false
286
+ }
287
+ end
288
+
289
+ # Mock parsing methods - these would be replaced with actual Tree-sitter parsing
290
+ def parse_ruby_source(source_code)
291
+ # This would use the actual ruby_tree_sitter gem
292
+ # For now, return a mock AST structure
293
+ {
294
+ type: "program",
295
+ children: extract_ruby_nodes(source_code)
296
+ }
297
+ end
298
+
299
+ def parse_javascript_source(source_code)
300
+ {
301
+ type: "program",
302
+ children: extract_javascript_nodes(source_code)
303
+ }
304
+ end
305
+
306
+ def parse_typescript_source(source_code)
307
+ {
308
+ type: "program",
309
+ children: extract_typescript_nodes(source_code)
310
+ }
311
+ end
312
+
313
+ def parse_python_source(source_code)
314
+ {
315
+ type: "module",
316
+ children: extract_python_nodes(source_code)
317
+ }
318
+ end
319
+
320
+ # Simple regex-based extraction for demonstration
321
+ # In practice, these would be replaced with actual Tree-sitter node extraction
322
+ def extract_ruby_nodes(source_code)
323
+ nodes = []
324
+ lines = source_code.lines
325
+
326
+ lines.each_with_index do |line, index|
327
+ line_number = index + 1
328
+
329
+ # Extract class definitions (including nested constants)
330
+ if (match = line.match(/^\s*class\s+((?:\w+::)*\w+)/))
331
+ nodes << {
332
+ type: "class",
333
+ name: match[1],
334
+ line: line_number,
335
+ start_column: line.index(match[0]),
336
+ end_column: line.index(match[0]) + match[0].length
337
+ }
338
+ end
339
+
340
+ # Extract module definitions (including nested constants)
341
+ if (match = line.match(/^\s*module\s+((?:\w+::)*\w+)/))
342
+ nodes << {
343
+ type: "module",
344
+ name: match[1],
345
+ line: line_number,
346
+ start_column: line.index(match[0]),
347
+ end_column: line.index(match[0]) + match[0].length
348
+ }
349
+ end
350
+
351
+ # Extract method definitions (including special methods)
352
+ if (match = line.match(/^\s*def\s+([\w\[\]=!?]+)/))
353
+ nodes << {
354
+ type: "method",
355
+ name: match[1],
356
+ line: line_number,
357
+ start_column: line.index(match[0]),
358
+ end_column: line.index(match[0]) + match[0].length
359
+ }
360
+ end
361
+
362
+ # Extract singleton/class method definitions
363
+ if (match = line.match(/^\s*def\s+(?:self|[\w:]+)\.([\w\[\]=!?]+)/))
364
+ nodes << {
365
+ type: "singleton_method",
366
+ name: match[1],
367
+ line: line_number,
368
+ start_column: line.index(match[0]),
369
+ end_column: line.index(match[0]) + match[0].length
370
+ }
371
+ end
372
+
373
+ # Extract require statements
374
+ if (match = line.match(/^\s*require\s+['"]([^'"]+)['"]/))
375
+ nodes << {
376
+ type: "require",
377
+ target: match[1],
378
+ line: line_number,
379
+ start_column: line.index(match[0]),
380
+ end_column: line.index(match[0]) + match[0].length
381
+ }
382
+ end
383
+
384
+ # Extract require_relative statements
385
+ if (match = line.match(/^\s*require_relative\s+['"]([^'"]+)['"]/))
386
+ nodes << {
387
+ type: "require_relative",
388
+ target: match[1],
389
+ line: line_number,
390
+ start_column: line.index(match[0]),
391
+ end_column: line.index(match[0]) + match[0].length
392
+ }
393
+ end
394
+ end
395
+
396
+ nodes
397
+ end
398
+
399
+ def extract_javascript_nodes(source_code)
400
+ nodes = []
401
+ lines = source_code.lines
402
+
403
+ lines.each_with_index do |line, index|
404
+ line_number = index + 1
405
+
406
+ # Extract class declarations
407
+ if (match = line.match(/class\s+(\w+)/))
408
+ nodes << {
409
+ type: "class",
410
+ name: match[1],
411
+ line: line_number,
412
+ start_column: line.index(match[0]),
413
+ end_column: line.index(match[0]) + match[0].length
414
+ }
415
+ end
416
+
417
+ # Extract function declarations
418
+ if (match = line.match(/function\s+(\w+)/))
419
+ nodes << {
420
+ type: "function",
421
+ name: match[1],
422
+ line: line_number,
423
+ start_column: line.index(match[0]),
424
+ end_column: line.index(match[0]) + match[0].length
425
+ }
426
+ end
427
+
428
+ # Extract import statements
429
+ if (match = line.match(/import\s+.*from\s+['"]([^'"]+)['"]/))
430
+ nodes << {
431
+ type: "import",
432
+ target: match[1],
433
+ line: line_number,
434
+ start_column: line.index(match[0]),
435
+ end_column: line.index(match[0]) + match[0].length
436
+ }
437
+ end
438
+ end
439
+
440
+ nodes
441
+ end
442
+
443
+ def extract_typescript_nodes(source_code)
444
+ # Similar to JavaScript but with TypeScript-specific patterns
445
+ extract_javascript_nodes(source_code)
446
+ end
447
+
448
+ def extract_python_nodes(source_code)
449
+ nodes = []
450
+ lines = source_code.lines
451
+
452
+ lines.each_with_index do |line, index|
453
+ line_number = index + 1
454
+
455
+ # Extract class definitions
456
+ if (match = line.match(/class\s+(\w+)/))
457
+ nodes << {
458
+ type: "class",
459
+ name: match[1],
460
+ line: line_number,
461
+ start_column: line.index(match[0]),
462
+ end_column: line.index(match[0]) + match[0].length
463
+ }
464
+ end
465
+
466
+ # Extract function definitions
467
+ if (match = line.match(/def\s+(\w+)/))
468
+ nodes << {
469
+ type: "function",
470
+ name: match[1],
471
+ line: line_number,
472
+ start_column: line.index(match[0]),
473
+ end_column: line.index(match[0]) + match[0].length
474
+ }
475
+ end
476
+
477
+ # Extract import statements
478
+ if (match = line.match(/import\s+([^#\n]+)/))
479
+ nodes << {
480
+ type: "import",
481
+ target: match[1].strip,
482
+ line: line_number,
483
+ start_column: line.index(match[0]),
484
+ end_column: line.index(match[0]) + match[0].length
485
+ }
486
+ end
487
+ end
488
+
489
+ nodes
490
+ end
491
+ end
492
+ end
493
+ end