Marty Andrews

artful code

Friday, September 19, 2008

Release of Roodi 1.3.0

I've just released version 1.3.0 of Roodi. It contains two new checks - a CaseMissingElse checks that ensures you have a default path through your case statements, and an AssignmentInConditional check that looks out for things like if foo = 1 which are likely to be mis-typed equality checks. I've also DRY'ed up the code a bit, but the major new feature is the ability to provide your own configuration file.

If a config file is not provided, Roodi now configures itself with via a YAML one that ships inside the gem which looks like this:

AssignmentInConditionalCheck:    { }CaseMissingElseCheck:            { }ClassLineCountCheck:             { line_count: 300 }ClassNameCheck:                  { pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ }CyclomaticComplexityBlockCheck:  { complexity: 4 }CyclomaticComplexityMethodCheck: { complexity: 8 }EmptyRescueBodyCheck:            { }ForLoopCheck:                    { }MethodLineCountCheck:            { line_count: 20 }MethodNameCheck:                 { pattern: !ruby/regexp /^[_a-z<>=\[\]|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/ }ModuleLineCountCheck:            { line_count: 300 }ModuleNameCheck:                 { pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ }ParameterNumberCheck:            { parameter_count: 5 }

It's basically a list of checks, each with a hash of options for that check. You can take this file as a starting point and remove existing check, add your own custom new checks, or change the default values on some of them. I've intentionally been strict on the values on some of the checks with thresholds, setting high standards that I'd expect to see on my own projects. If you're working through a long list of warnings and want to eliminate some of the noise, or if you decide the thresholds should be different, you can save this in your own config file and use it to configure roodi.

To use your own config file with roodi, pass it in as a value to the -config parameter on the command line like this:

roodi -config=my_roodi_config.yml "rails_app/**/*.rb"

Friday, September 12, 2008

Roodi version numbering changes.

After releasing Roodi a couple of days ago, and getting some feedback from people, I realised that they had the wrong version of it. When I first started getting the project set up as a gem with Hoe, I pushed a 1.x version up to Rubyforge. Since that time, I tried to change the version numbering back to 0.x, and deleted the old files on Rubyforge. Too late though, Rubyforge has cached the old gem file with a higher version number. That means people who've installed Roodi in the last couple of days actually got an older version.

Since then, I've changed the version numbering (again) back to a 1.x version, and published a new gem with a higher version number than the old one. It actually contains a couple of enhancements and some new checks as well. Version 1.2.0 is now published, and is the latest stable version.

Wednesday, September 10, 2008

First official release of Roodi

Roodi is now a few weeks old, and ready for it's first official release. It statically parses your Ruby code and warns you about design issues that you might have. to install Roodi, run this command:

gem install roodi

You can then run the roodi command and give it a list of patterns to find files to check. For example, you could:

Check all ruby files in a rails app:

roodi "rails_app/**/*.rb"

...or check one controller and all model files in a rails app:

roodi app/controller/sample_controller.rb "app/models/*.rb"

Here's an example of what Roodi output looks like when it is run against the Hpricot source code:

pudbookpro:Data marty$ roodi "hpricot/**/*.rb"hpricot/extras/mingw-rbconfig.rb:155 - Block cyclomatic complexity is 5.  It should be 4 or less.hpricot/lib/hpricot/builder.rb:75 - Block cyclomatic complexity is 5.  It should be 4 or less.hpricot/lib/hpricot/parse.rb:52 - Block cyclomatic complexity is 40.  It should be 4 or less.hpricot/lib/hpricot/traverse.rb:50 - Block cyclomatic complexity is 5.  It should be 4 or less.hpricot/lib/hpricot/traverse.rb:315 - Block cyclomatic complexity is 5.  It should be 4 or less.hpricot/lib/hpricot/traverse.rb:555 - Block cyclomatic complexity is 11.  It should be 4 or less.hpricot/setup.rb:215 - Block cyclomatic complexity is 6.  It should be 4 or less.hpricot/setup.rb:578 - Block cyclomatic complexity is 5.  It should be 4 or less.hpricot/lib/hpricot/builder.rb:63 - Method name "tag!" has a cyclomatic complexity is 15.  It should be 8 or less.hpricot/lib/hpricot/traverse.rb:247 - Method name "search" has a cyclomatic complexity is 27.  It should be 8 or less.hpricot/lib/hpricot/traverse.rb:553 - Method name "each_hyperlink_attribute" has a cyclomatic complexity is 11.  It should be 8 or less.hpricot/lib/hpricot/traverse.rb:774 - Method name "author" has a cyclomatic complexity is 12.  It should be 8 or less.hpricot/setup.rb:147 - Method name "standard_entries" has a cyclomatic complexity is 13.  It should be 8 or less.hpricot/setup.rb:858 - Method name "parsearg_global" has a cyclomatic complexity is 10.  It should be 8 or less.hpricot/setup.rb:1265 - Method name "update_shebang_line" has a cyclomatic complexity is 9.  It should be 8 or less.hpricot/lib/hpricot/traverse.rb:781 - Rescue block should not be empty.hpricot/lib/hpricot/traverse.rb:791 - Rescue block should not be empty.hpricot/lib/hpricot/traverse.rb:800 - Rescue block should not be empty.hpricot/lib/hpricot/traverse.rb:807 - Rescue block should not be empty.hpricot/lib/hpricot.rb:17 - Rescue block should not be empty.hpricot/setup.rb:592 - Rescue block should not be empty.hpricot/setup.rb:601 - Rescue block should not be empty.hpricot/setup.rb:613 - Rescue block should not be empty.hpricot/lib/hpricot/builder.rb:63 - Method name "tag!" has 31 lines.  It should have 20 or less.hpricot/lib/hpricot/traverse.rb:247 - Method name "search" has 80 lines.  It should have 20 or less.hpricot/setup.rb:147 - Method name "standard_entries" has 51 lines.  It should have 20 or less.hpricot/setup.rb:948 - Method name "print_usage" has 31 lines.  It should have 20 or less.hpricot/test/test_parser.rb:69 - Method name "scan_basic" has 50 lines.  It should have 20 or less.hpricot/lib/hpricot/parse.rb:3 - Method name "Hpricot" should match pattern /^[_a-z<>=\[\]|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/

The default parameters for checks can be changed, and checks can be turned on and off. Lots of additional checks are planned, as is support for rake tasks and rails plugins.

Please drop me a line if you use Roodi and find it useful. I'd love to get some feedback on how people are using it. Happy checking!

Tuesday, September 9, 2008

Line numbers with ParseTree

I reluctantly moved Roodi over to ParseTree recently because I figured that would make it more accessible to people. My reluctance came from the fact that I was going to lose the line number support the I had in JRuby. Happy days - it turns out I was wrong!

The following bit of code sets up ParseTree so you can parse some code:

parse_tree = ParseTree.newtree = parse_tree.parse_tree_for_string(ruby_code_as_text, filename)

It turns out that one very small tweak will give me line number support:

parse_tree = ParseTree.new(true)tree = parse_tree.parse_tree_for_string(ruby_code_as_text, filename)

The addition of that boolean means that ParseTree will now include newline nodes in the tree structure that it gives me. Every newline node includes the line number and the name of the file that it came from. That made the output from Roodi much more useful.

Sunday, September 7, 2008

Roodi on ParseTree instead of JRuby

I started out building Roodi using the JRuby apis to parse Ruby code and build an AST. My main alternative to that was to use ParseTree. The advantage of using JRuby was twofold. Firstly - I could get access to newline characters as part of the AST, which I wanted to use in checks. Secondly - I could get access to line numbers, which I wanted to use as part of the error message output.

I've just sacrificed both of those advantages and switched the code base to use ParseTree instead. Why? Because it makes the tool more widely usable. I had my own non-JRuby projects that I was trying to run Roodi in, and it became a pain in the ass. Now I get much better portability, which is worth the sacrifices I had to make. I suspect I can patch line numbers in later, and I'm not that fussed about losing the checks that needed to check the newline characters.

Thursday, September 4, 2008

Roodi - Checkstyle for Ruby

I've been working on a tool for the last couple of weeks that I'm calling Roodi (Ruby Object Oriented Design Inferometer). It's very much a tool for Ruby like checkstyle is for Java. The framework was surprisingly quick to get up and going, and I've written seven checks (class name, method name, method cyclomatic complexity, block cyclomatic complexity, empty rescue body, for loop, method line count) for it so far. It's working pretty well for all of them.

Here's a sample of what it looks like (I've trimmed some of the output for brevity):

pudbookpro:Data marty$ ./roodi/bin/roodi "rspec/**/*.rb"rspec/lib/spec/rake/spectask.rb:152 - Block cyclomatic complexity is 11.  It should be 4 or less.rspec/lib/spec/rake/verify_rcov.rb:37 - Block cyclomatic complexity is 6.  It should be 4 or less.rspec/lib/spec/matchers/be.rb:57 - Method name "match_or_compare" has a cyclomatic complexity is 12.  It should be 8 or less.rspec/lib/spec/matchers/change.rb:12 - Method name "matches?" has a cyclomatic complexity is 9.  It should be 8 or less.rspec/lib/spec/matchers/have.rb:28 - Method name "matches?" has a cyclomatic complexity is 11.  It should be 8 or less.rspec/lib/spec/expectations/errors.rb:6 - Rescue block should not be empty.rspec/lib/spec/rake/spectask.rb:186 - Rescue block should not be empty.rspec/lib/spec/expectations/differs/default.rb:20 - Method name "diff_as_string" has 21 lines.  It should have 20 or less.rspec/lib/spec/matchers/change.rb:35 - Method name "failure_message" has 24 lines.  It should have 20 or less.rspec/lib/spec/matchers/include.rb:31 - Method name "_message" should match pattern (?-mix:^[a-z]+[a-z0-9_]*[!\?]?$).rspec/lib/spec/matchers/include.rb:35 - Method name "_pretty_print" should match pattern (?-mix:^[a-z]+[a-z0-9_]*[!\?]?$).

You can get Roodi from github for now. I'm still in the process of getting it packaged and ready as a gem.

One of the interesting features (and issues) with Roodi is that it is dependent on JRuby. That's because it uses the JRuby AST libraries to parse Ruby source code. There's no good way (IMHO) yet to build an AST of Ruby source code in Ruby. ParseTree is close, but gives you an AST of the executable code, not the source. What does that mean? Well, for example, I can't get any representation of whitespace characters from ParseTree. Nor can I get line numbers from it. JRuby isn't perfect either, but it gives me line numbers and newlines at least.

The use of JRuby libraries makes the Roodi design interesting. Roodi is completely written in Ruby, but the calls out to libraries written in Java means it needs to run under JRuby. So I end up running JRuby to execute a Ruby app, which then calls out to JRuby to parse Ruby code.

I'd love some feedback if anyone has a play. Writing checks is really easy too. I'll happily roll them in if someone wants to write some.

Monday, March 3, 2008

ruby files as libraries or scripts

Include the following snippet at the end of a Ruby file:

if $0 == __FILE__# do somethingend

If you execute the file, the 'do something' block will run, but if you require the file, it won't. That way you can use the file as both a library and a script.

Sunday, February 10, 2008

Classification does not equal prioritisation

One of the classic mistakes that people make in planning agile software projects comes about when someone is asked to prioritise the stories to be done by the software team. The conversation usually goes like this:

Q: What do you think about Story A?

A: That's a priority one.

Q: Ok, what do you think about Story B?

A: That's a priority one as well.

At this point, the stories have not been prioritised. They have been classified into groups, where the group is named "Priority One". Whilst this may be a useful culling technique, do not fool yourself into thinking they are prioritised. What's worse for you is that, whether you like it or not, the stories will get done in some order. If the team is ready to start on a story and finds two of them that are both "Priority One", then they will pick one of them to do first. Sometimes, they will pick the wrong one.

Do your team a favour. Don't classify your stories. Prioritise them.