rsutphin-cf_case_check 0.0.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.
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