Customizable Static Code Analysis for Ruby on Rails
As software projects grow and evolve, maintaining code quality and consistency becomes increasingly challenging. Code analysis is essential for identifying potential issues, ensuring adherence to best practices, and facilitating smooth upgrades to newer versions of frameworks and libraries. Common methods for code analysis include manual code reviews, linters, and automated testing suites.
In this post, I introduce a solution for static code analysis tailored to Ruby on Rails projects. This approach uses customizable detectors to automate the detection of specific syntax patterns that may need attention. As an example, consider the transition from Rails 5.2 to 6.1. While where.not
with multiple attributes remains supported, updating these patterns across a large codebase can be time-consuming and error-prone. For more details on this change, you can refer to Rails pull request #36029.
The solution presented here offers a framework for automating static code analysis, enabling developers to efficiently identify and refactor complex or deprecated code patterns in their projects.
A Flexible Solution for Static Code Analysis
This solution provides a customizable code architecture for scanning Ruby files and identifying specific syntax patterns using detectors. It offers a framework for efficiently identifying and addressing problematic code patterns while allowing for easy extension and modification to suit project-specific needs.
Key Features
- Custom Detectors: Easily define detectors to identify specific syntax patterns.
- Automated Scanning: Streamline the process of analyzing Ruby files for syntax issues.
- Extendable Architecture: Adapt the solution to fit various project requirements and coding standards.
Core Components
SyntaxScanner Class
The SyntaxScanner
class serves as the core of the analysis tool, responsible for traversing directories and applying detectors to identify code patterns.
# lib/syntax_scanner.rb
require 'parser/current'
require 'colorize'
class SyntaxScanner
attr_reader :directory, :results, :structure_detector
def initialize(directory, structure_detector)
@directory = directory
@structure_detector = structure_detector
@results = []
end
def scan
log_message("Scanning directory: #{@directory}", :blue)
ruby_files.each do |file|
process_file(file)
end
output_results
end
private
def ruby_files
Dir.glob(File.join(@directory, '**', '*.rb'))
end
def process_file(file)
content = File.read(file)
ast = Parser::CurrentRuby.parse(content)
find_syntax_structures(ast, file) if ast
rescue => e
log_message("Error processing file #{file}: #{e.message}", :red)
end
def find_syntax_structures(node, file)
return unless node.is_a?(Parser::AST::Node)
if @structure_detector.matches?(node)
record_result(node, file)
end
node.children.each do |child|
find_syntax_structures(child, file) if child.is_a?(Parser::AST::Node)
end
end
def record_result(node, file)
line_number = node.location.expression.line
result = {
file: file,
line: line_number,
code: node.location.expression.source_buffer.source_line(line_number).strip
}
@results << result
log_message("Found matching syntax structure in #{file} at line #{line_number}:", :green)
log_message(result[:code], :yellow)
end
def output_results
log_message("\nSummary of Results:", :blue)
if @results.empty?
log_message("No occurrences of the specified syntax structure found.", :blue)
else
log_message("Found #{@results.size} occurrences of the specified syntax structure:", :blue)
@results.each do |result|
log_message("#{result[:file]}:#{result[:line]} - #{result[:code]}", :yellow)
end
end
end
def log_message(message, color)
puts message.colorize(color)
end
end
WhereNotSyntaxDetector
The WhereNotSyntaxDetector
is an example of a detector designed to identify specific instances of where.not
with multiple attributes.
# lib/where_not_syntax_detector.rb
class WhereNotSyntaxDetector
def matches?(node)
return false unless node.type == :send
receiver, method_name, *args = *node
if method_name == :not && receiver.type == :send && receiver.children[1] == :where
second_arg = node.children[2]
return second_arg&.type == :hash && second_arg.children.size > 1
end
false
end
end
Usage Example
To use this framework, instantiate the WhereNotSyntaxDetector
and pass it to the SyntaxScanner
:
# Usage example
detector = WhereNotSyntaxDetector.new
scanner = SyntaxScanner.new('app', detector)
scanner.scan
Example Output
Here’s an example of what the output might look like when running the scanner:
Scenario
- Directory: Scanning the
app
directory for specific syntax patterns.
Console Log Output
Scanning directory: app
Found matching syntax structure in app/models/user.rb at line 45:
where.not(first_name: nil, last_name: nil)
Summary of Results:
Found 1 occurrences of the specified syntax structure:
app/models/user.rb:45 - where.not(first_name: nil, last_name: nil)
Extending the Framework
Creating a Custom Detector
To adapt this framework for other syntax patterns, implement a custom detector class:
# lib/custom_syntax_detector.rb
class CustomSyntaxDetector
def matches?(node)
# Implement logic to match your specific syntax structure
false
end
end
Using a Custom Detector
Replace the existing detector with your custom implementation:
# Usage example with custom detector
custom_detector = CustomSyntaxDetector.new
scanner = SyntaxScanner.new('app', custom_detector)
scanner.scan
Conclusion
Automatic code analysis can significantly enhance the efficiency and reliability of maintaining and upgrading Ruby projects. By using the SyntaxScanner
script as a starting point, developers can automate the detection of complex or deprecated syntax patterns, reducing the time and effort required for manual code reviews.
This script offers a valuable foundation for identifying potential issues in a codebase, allowing for customization and extension according to specific needs. I hope this tool inspires ideas for implementing similar solutions in various projects, ultimately improving code quality and streamlining the development process.
Feel free to experiment with and adapt the script to fit unique requirements, and I hope its insights prove beneficial. If there are any thoughts or improvements to share, I’d love to see them in the comments. Happy coding!
Comments