rsutphin-cf_case_check 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ pkg
2
+ coverage
3
+ *.gem
data/History.txt ADDED
@@ -0,0 +1,3 @@
1
+ 0.0.0 / 2008-12-14
2
+ ==================
3
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,26 @@
1
+ .gitignore
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/cf_case_check
7
+ lib/case_check.rb
8
+ lib/case_check/coldfusion_source.rb
9
+ lib/case_check/commands.rb
10
+ lib/case_check/configuration.rb
11
+ lib/case_check/core-ext.rb
12
+ lib/case_check/reference.rb
13
+ lib/case_check/references/cfc.rb
14
+ lib/case_check/references/cfinclude.rb
15
+ lib/case_check/references/cfmodule.rb
16
+ lib/case_check/references/custom_tag.rb
17
+ spec/coldfusion_source_spec.rb
18
+ spec/commands_spec.rb
19
+ spec/configuration_spec.rb
20
+ spec/core_ext_spec.rb
21
+ spec/reference_spec.rb
22
+ spec/references/cfc_spec.rb
23
+ spec/references/cfinclude_spec.rb
24
+ spec/references/cfmodule_spec.rb
25
+ spec/references/custom_tag_spec.rb
26
+ spec/spec_helper.rb
data/README.txt ADDED
@@ -0,0 +1,75 @@
1
+ `cf_case_check`
2
+ ===============
3
+ http://github.com/rsutphin/cf_case_check
4
+
5
+ Description
6
+ -----------
7
+
8
+ `cf_case_check` is a utility which walks a ColdFusion application's source and
9
+ determines which references to other files will not work with a case-sensitive
10
+ filesystem. Its intended audience is developers/sysadmins who are migrating
11
+ a CF application from Windows hosting to Linux or another UNIX.
12
+
13
+ `cf_case_check` was developed at the [Northwestern University Biomedical
14
+ Informatics Center][NUBIC].
15
+
16
+ [NUBIC]: http://www.nucats.northwestern.edu/centers/nubic/index.html
17
+
18
+ Features
19
+ --------
20
+
21
+ * Resolves references of the following types:
22
+ - `CF_`-style custom tags
23
+ - `cfinclude`
24
+ - `cfmodule` (both `template` and `name`)
25
+ - `createObject` (for CFCs only)
26
+ * Prints report to stdout
27
+ * Allows for designation of custom tag & CFC search paths outside the
28
+ application root
29
+
30
+ Synopsis
31
+ --------
32
+
33
+ myapp$ cf_case_check
34
+
35
+ For command-line options, do:
36
+
37
+ $ cf_case_check --help
38
+
39
+ Requirements
40
+ ------------
41
+
42
+ * Ruby 1.8.6 or later (may work with earlier, but not tested)
43
+
44
+ Install
45
+ -------
46
+
47
+ Follow the GitHub rubygems [setup directions](http://gems.github.com/), then
48
+
49
+ $ sudo gem install rsutphin-cf_case_check
50
+
51
+ License
52
+ -------
53
+
54
+ (The MIT License)
55
+
56
+ Copyright (c) 2008 Rhett Sutphin
57
+
58
+ Permission is hereby granted, free of charge, to any person obtaining
59
+ a copy of this software and associated documentation files (the
60
+ 'Software'), to deal in the Software without restriction, including
61
+ without limitation the rights to use, copy, modify, merge, publish,
62
+ distribute, sublicense, and/or sell copies of the Software, and to
63
+ permit persons to whom the Software is furnished to do so, subject to
64
+ the following conditions:
65
+
66
+ The above copyright notice and this permission notice shall be
67
+ included in all copies or substantial portions of the Software.
68
+
69
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
70
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
71
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
72
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
73
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
74
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
75
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ Bones.setup
8
+ rescue LoadError
9
+ load 'tasks/setup.rb'
10
+ end
11
+
12
+ ensure_in_path 'lib'
13
+ require 'case_check'
14
+
15
+ task :default => 'spec:run'
16
+ task :install => 'gem:install'
17
+
18
+ PROJ.name = 'cf_case_check'
19
+ PROJ.authors = 'Rhett Sutphin'
20
+ PROJ.email = 'rhett@detailedbalance.net'
21
+ PROJ.url = 'http://github.com/rsutphin/cf_case_check'
22
+ PROJ.version = CaseCheck::VERSION
23
+ # PROJ.rubyforge.name = 'cf_case_check'
24
+ PROJ.description = "A utility which walks a ColdFusion application's source and determines which includes, custom tags, etc, will not work with a case-sensitive filesystem"
25
+ PROJ.exclude << "gem$" << "gemspec$"
26
+
27
+ PROJ.ruby_opts = [] # There are a bunch of warnings in rspec, so setting -w isn't useful
28
+ PROJ.spec.opts << '--color'
29
+ PROJ.rcov.opts << '--exclude /Library'
30
+
31
+ PROJ.gem.dependencies << 'activesupport'
32
+
33
+ desc 'Regenerate the gemspec for github'
34
+ task :'gem:spec' => 'gem:prereqs' do
35
+ PROJ.gem._spec.files = PROJ.gem._spec.files.reject { |f| f =~ /^tasks/ }
36
+ File.open("#{PROJ.name}.gemspec", 'w') do |gemspec|
37
+ gemspec.puts PROJ.gem._spec.to_ruby
38
+ end
39
+ end
data/bin/cf_case_check ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(
4
+ File.join(File.dirname(__FILE__), %w[.. lib case_check]))
5
+
6
+ def print_report(checker, out=$stdout)
7
+ CaseCheck.status_stream.puts "#{checker.reference_count} references"
8
+ checker.sources.each do |s|
9
+ out.puts s.src.filename
10
+ s.internal_references.each do |ir|
11
+ out.puts " #{bullet(ir)} #{ir.message}"
12
+ end
13
+ end
14
+ end
15
+
16
+ def bullet(ir)
17
+ case ir.resolution
18
+ when :exact
19
+ '-'
20
+ when :case_insensitive
21
+ '+'
22
+ else
23
+ '*'
24
+ end
25
+ end
26
+
27
+ print_report CaseCheck::Checker.new(CaseCheck::Params.new(ARGV))
@@ -0,0 +1,92 @@
1
+ require 'enumerator'
2
+
3
+ module CaseCheck
4
+
5
+ class ColdfusionSource
6
+ attr_accessor :internal_references, :content, :filename
7
+
8
+ def self.create(filename)
9
+ f = File.expand_path(filename)
10
+ new(f, File.read(f))
11
+ end
12
+
13
+ def initialize(filename, content = nil)
14
+ @filename = filename
15
+ self.content = content
16
+ end
17
+
18
+ def analyze
19
+ [CustomTag, Cfmodule, Cfinclude, Cfc].each do |reftype|
20
+ internal_references.concat reftype.search(self)
21
+ end
22
+ end
23
+
24
+ def internal_references
25
+ @internal_references ||= []
26
+ end
27
+
28
+ def inexact_internal_references
29
+ internal_references.reject { |ir| ir.resolution == :exact }
30
+ end
31
+
32
+ # returns the line number (1-based) on which the given character index lies
33
+ def line_of(i)
34
+ return nil if i >= content.size
35
+ char_ct = 0
36
+ l = 0
37
+ while char_ct <= i
38
+ char_ct += lines[l].size
39
+ l += 1
40
+ end
41
+ l
42
+ end
43
+
44
+ def content=(c)
45
+ @lines = nil
46
+ @content = c
47
+ end
48
+
49
+ def lines
50
+ return @lines if @lines
51
+ @lines = []
52
+ content.split(/(\r\n|\r|\n)/).each_slice(2) { |line_and_br| @lines << line_and_br.join('') }
53
+ @lines
54
+ end
55
+
56
+ # Scans the content for the given RE, yielding the MatchData for each match
57
+ # and the line number on which it occurred
58
+ def scan(re, &block)
59
+ results = []
60
+ char_offset = 0
61
+ re.scan(content) do |md|
62
+ results << (yield [md, line_of(char_offset + md.begin(0))])
63
+
64
+ char_offset += md[0].size + md.pre_match.size
65
+ remaining = md.post_match
66
+ end
67
+ results
68
+ end
69
+
70
+ # Scans the content for opening and/or self-closing tags with the given
71
+ # name. Yields the full text of the tag, a parsed representation of the
72
+ # attributes, and the line number back to the provided block.
73
+ def scan_for_tag(tag, &block)
74
+ scan(/<#{tag}(.*?)>/mi) do |md, l|
75
+ attributes = %w(' ").collect do |q|
76
+ /(\w+)\s*=\s*#{q}([^#{q}]*?)#{q}/.scan(md[1].gsub(%r(/$), ''))
77
+ end.flatten.inject({}) do |attrs, amd|
78
+ attrs[normalize_attribute_key(amd[1])] = amd[2]
79
+ attrs
80
+ end
81
+ yield md[0], attributes, l
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def normalize_attribute_key(key)
88
+ key.downcase.to_sym
89
+ end
90
+ end
91
+
92
+ end
@@ -0,0 +1,103 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ module CaseCheck
5
+
6
+ class Params
7
+ def initialize(argv, name='cf_case_check')
8
+ @options = OpenStruct.new
9
+
10
+ opts = OptionParser.new do |opts|
11
+ opts.banner = "Usage: #{name} [options]"
12
+
13
+ opts.on("-d", "--dir DIRECTORY",
14
+ "The application root from which to search. Defaults to the current directory.") do |p|
15
+ @options.directory = p
16
+ end
17
+
18
+ opts.on("-c", "--config CONFIGYAML",
19
+ "The configuration file which includes the directories to search for custom tags and CFCs.") do |p|
20
+ @options.configfile = p
21
+ end
22
+
23
+ opts.on("-v", "--verbose", "Show all references, including the ones which can be resolved exactly.") do |v|
24
+ @options.verbose = true
25
+ end
26
+
27
+ opts.on_tail("-h", "--help", "Show this message") do
28
+ CaseCheck.status_stream.puts opts
29
+ CaseCheck.exit
30
+ end
31
+
32
+ opts.on_tail("--version", "Show version") do
33
+ CaseCheck.status_stream.puts "#{name} #{CaseCheck.version}"
34
+ CaseCheck.exit
35
+ end
36
+ end.parse!(argv)
37
+
38
+ read_config!
39
+ end
40
+
41
+ def directory
42
+ @options.directory || '.'
43
+ end
44
+
45
+ def configuration_file
46
+ @options.configfile || File.join(directory, "cf_case_check.yml")
47
+ end
48
+
49
+ def verbose?
50
+ @options.verbose
51
+ end
52
+
53
+ private
54
+
55
+ def read_config!
56
+ @configuration =
57
+ if File.exist?(configuration_file)
58
+ Configuration.new(configuration_file)
59
+ end
60
+ end
61
+ end
62
+
63
+ class Checker
64
+ def initialize(params)
65
+ @params = params
66
+ CaseCheck.status_stream.print "Reading source files "
67
+ @sources = Dir["#{params.directory}/**/*.cf[mc]"].collect do |f|
68
+ CaseCheck.status_stream.print '.'
69
+ ColdfusionSource.create(f)
70
+ end
71
+ CaseCheck.status_stream.puts
72
+ CaseCheck.status_stream.print "Analyzing "
73
+ @sources.each do |s|
74
+ CaseCheck.status_stream.print '.'
75
+ s.analyze
76
+ end
77
+ CaseCheck.status_stream.puts "\n"
78
+ end
79
+
80
+ def sources
81
+ @sources.reject { |src| @params.verbose? ? src.internal_references.empty? : src.inexact_internal_references.empty? }.
82
+ collect { |src| FilteredSource.new(src, @params.verbose?) }
83
+ end
84
+
85
+ def reference_count
86
+ sources.inject(0) { |c, s| c + s.internal_references.size }
87
+ end
88
+ end
89
+
90
+ class FilteredSource
91
+ attr_reader :src
92
+
93
+ def initialize(source, include_exact_matches)
94
+ @src = source
95
+ @include_exact = include_exact_matches
96
+ end
97
+
98
+ def internal_references
99
+ @include_exact ? src.internal_references : src.inexact_internal_references
100
+ end
101
+ end
102
+
103
+ end
@@ -0,0 +1,46 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module CaseCheck
5
+
6
+ class Configuration
7
+ def initialize(filename)
8
+ @filename = filename
9
+ @doc = YAML.load_file(@filename)
10
+ apply
11
+ end
12
+
13
+ def [](k)
14
+ @doc[k]
15
+ end
16
+
17
+ private
18
+
19
+ def apply
20
+ read_custom_tag_dirs
21
+ read_cfc_dirs
22
+ end
23
+
24
+ def read_custom_tag_dirs
25
+ CustomTag.directories = absolutize_directories(@doc['custom_tag_directories'] || [])
26
+ end
27
+
28
+ def read_cfc_dirs
29
+ Cfc.directories = absolutize_directories(@doc['cfc_directories'] || [])
30
+ end
31
+
32
+ private
33
+
34
+ def absolutize_directories(dirs)
35
+ dirs.to_a.collect { |d|
36
+ p = Pathname.new(d)
37
+ if p.absolute?
38
+ p
39
+ else
40
+ Pathname.new(File.dirname(@filename)) + p
41
+ end
42
+ }.collect { |p| p.to_s }
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,51 @@
1
+ # Extend File with case-insensitive utility functions
2
+ class File
3
+ # Determines if the given filename maps exactly to an existing file, even
4
+ # if the underlying filesystem is case-insensitive
5
+ def self.exists_exactly?(name, base=nil)
6
+ first, rest = File.expand_path(name, '/')[1, name.size].split('/', 2)
7
+ base ||= ''
8
+ actual_files = Dir[File.join(base, '*')]
9
+ candidate = File.join(base, first)
10
+ actual_files.include?(candidate) && (!rest || self.exists_exactly?(rest, candidate))
11
+ end
12
+
13
+ # Finds the true filename for the file which can be accessed as "name"
14
+ # case-insensitively
15
+ def self.case_insensitive_canonical_name(name, base=nil)
16
+ first, rest = File.expand_path(name, '/')[1, name.size].split('/', 2)
17
+ base ||= ''
18
+ actual_files = Dir[File.join(base, '*')].collect { |fn| fn[(base.length + 1) .. -1] }
19
+ match =
20
+ if actual_files.include?(first)
21
+ first
22
+ else
23
+ actual_files.detect { |fn| fn.downcase == first.downcase }
24
+ end
25
+ if rest && match
26
+ case_insensitive_canonical_name(rest, File.join(base, match))
27
+ elsif match
28
+ File.join(base, match)
29
+ else
30
+ nil
31
+ end
32
+ end
33
+ end
34
+
35
+ class Regexp
36
+ # Like String#scan, except that it returns an array of MatchData instead of strings
37
+ # Works incrementally (returning nil) if given a block
38
+ def scan(s)
39
+ data = []
40
+ remaining = s
41
+ while md = self.match(remaining)
42
+ if block_given?
43
+ yield md
44
+ else
45
+ data << md
46
+ end
47
+ remaining = md.post_match
48
+ end
49
+ block_given? ? nil : data
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ # Models of various sorts of file references within CF code
2
+
3
+ require 'rubygems'
4
+ require 'activesupport'
5
+
6
+ module CaseCheck
7
+
8
+ # base class
9
+ class Reference < Struct.new(:source, :text, :line)
10
+ # abstract methods
11
+ # - expected_path
12
+ # returns the exact relative path to which this reference refers
13
+ # - resolved_to
14
+ # returns the absolute file to which this reference seems to point, if one could be found
15
+
16
+ # Returns :exact, :case_insensitive, or nil depending on whether
17
+ # the reference could be resolved on a case_sensitive FS,
18
+ # only on a case_insensitive FS, or not at all
19
+ def resolution
20
+ return nil unless resolved_to
21
+ case_sensitive_match? ? :exact : :case_insensitive
22
+ end
23
+
24
+ def message
25
+ start =
26
+ case resolution
27
+ when :exact
28
+ "Exactly resolved"
29
+ when :case_insensitive
30
+ "Case-insensitively resolved"
31
+ else
32
+ "Unresolved"
33
+ end
34
+ msg = "#{start} #{type_name} on line #{line}"
35
+ if resolution
36
+ "#{msg} from #{text} to #{resolved_to}"
37
+ else
38
+ "#{msg}: #{text}"
39
+ end
40
+ end
41
+
42
+ def type_name
43
+ self.class.name.split('::').last.underscore.gsub('_', ' ')
44
+ end
45
+
46
+ protected
47
+
48
+ def case_sensitive_match?
49
+ resolved_to.ends_with?(expected_path_tail)
50
+ end
51
+
52
+ def expected_path_tail
53
+ expected_path.split('/').reject { |pe| pe == '.' }.join('/').gsub(%r((\.\./)+), '')
54
+ end
55
+
56
+ def resolve_in(dir)
57
+ File.case_insensitive_canonical_name(File.expand_path(expected_path, dir))
58
+ end
59
+ end
60
+
61
+ end # module CaseCheck
62
+
63
+ CaseCheck.require_all_libs_relative_to(__FILE__, 'references')
@@ -0,0 +1,47 @@
1
+ module CaseCheck
2
+
3
+ # Reference as createObject("component", '...')
4
+ class Cfc < Reference
5
+ attr_reader :expected_path, :resolved_to
6
+
7
+ class << self
8
+ attr_writer :directories
9
+
10
+ def directories
11
+ @directories ||= []
12
+ end
13
+
14
+ def search(source)
15
+ source.scan(/createObject\((.*?)\)/mi) do |match, l|
16
+ args = match[1].split(/\s*,\s*/).collect { |a| a.gsub(/['"]/, '') }
17
+ unless args.size == 2 && args.first =~ /component/i
18
+ $stderr.puts "Non-CFC call on line #{l} of #{source.filename}: #{match[0]}"
19
+ end
20
+ new(source, args.last, l)
21
+ end
22
+ end
23
+ end
24
+
25
+ def initialize(source, text, line_number)
26
+ super
27
+ @expected_path = text.gsub('.', '/') + ".cfc"
28
+ @resolved_to = self.class.directories.inject(nil) do |resolved, dir|
29
+ resolved || resolve_in(dir)
30
+ end
31
+ end
32
+
33
+ protected
34
+
35
+ def case_sensitive_match?
36
+ return true if super
37
+ # According to the CF docs:
38
+ # "On UNIX systems, ColdFusion searches first for a file with a name
39
+ # that matches the specified component name, but is all lowercase.
40
+ # If it does not find the file, it looks for a filename that matches
41
+ # the component name exactly, with the identical character casing."
42
+ resolved_to.ends_with?(expected_path_tail.downcase)
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,20 @@
1
+ module CaseCheck
2
+
3
+ # Reference as <cfinclude template=
4
+ class Cfinclude < Reference
5
+ attr_accessor :expected_path, :resolved_to
6
+
7
+ def self.search(source)
8
+ source.scan_for_tag('cfinclude') do |text, attributes, line_number|
9
+ new(source, attributes[:template], line_number)
10
+ end
11
+ end
12
+
13
+ def initialize(source, text, line)
14
+ super
15
+ @expected_path = text
16
+ @resolved_to = resolve_in(File.dirname(source.filename))
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,48 @@
1
+ module CaseCheck
2
+
3
+ # Reference as <cfmodule name= or <cfmodule template=
4
+ class Cfmodule < Reference
5
+ attr_reader :expected_path, :resolved_to
6
+
7
+ class << self
8
+ def search(source)
9
+ source.scan_for_tag('cfmodule') do |text, attributes, line_number|
10
+ if attributes.keys.include?(:name)
11
+ Name.new(source, attributes[:name], line_number)
12
+ elsif attributes.keys.include?(:template)
13
+ Template.new(source, attributes[:template], line_number)
14
+ else
15
+ $stderr.puts "Neither name nor template for cfmodule on line #{line_number} of #{source.filename}"
16
+ end
17
+ end.compact
18
+ end
19
+ end
20
+
21
+ class Name < Cfmodule
22
+ def initialize(source, text, line)
23
+ super
24
+ @expected_path = text.gsub('.', '/') + ".cfm"
25
+ @resolved_to = CustomTag.directories.inject(nil) do |resolved, dir|
26
+ resolved || resolve_in(dir)
27
+ end
28
+ end
29
+
30
+ def type_name
31
+ "cfmodule with name"
32
+ end
33
+ end
34
+
35
+ class Template < Cfmodule
36
+ def initialize(source, text, line)
37
+ super
38
+ @expected_path = text
39
+ @resolved_to = resolve_in(File.dirname(source.filename))
40
+ end
41
+
42
+ def type_name
43
+ "cfmodule with template"
44
+ end
45
+ end
46
+ end
47
+
48
+ end