jdzak-cf_case_check 0.1.1

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,19 @@
1
+ 0.1.0 / 2008-12-15
2
+ ==================
3
+ * Add support for configurable substitutions for, e.g., application variable
4
+ use in references
5
+ * Include 'tem' (along with 'cfm' and 'cfc') in the list of extensions to use
6
+ when looking for CF source
7
+ * Ensure that source filenames are globbed case-insensitively
8
+
9
+ 0.0.2 / 2008-12-15
10
+ ==================
11
+ * Resolve CFCs in the same directory as the source
12
+
13
+ 0.0.1 / 2008-12-14
14
+ ==================
15
+ * Fix recursion bug in CaseCheck::exit
16
+
17
+ 0.0.0 / 2008-12-14
18
+ ==================
19
+ * 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,40 @@
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
+ PROJ.gem._spec.rubyforge_project = nil
37
+ File.open("#{PROJ.name}.gemspec", 'w') do |gemspec|
38
+ gemspec.puts PROJ.gem._spec.to_ruby
39
+ end
40
+ 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,127 @@
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.source_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("-a", "--auto-configure DIRECTORY",
24
+ "Automatically configure the directories to search for custom tag and CFCs using Coldfusion's configuration. (Coldfusion 8 Only)") do |p|
25
+ @options.coldfusion_directory = p
26
+ end
27
+
28
+ opts.on("-v", "--verbose", "Show all references, including the ones which can be resolved exactly.") do |v|
29
+ @options.verbose = true
30
+ end
31
+
32
+ opts.on_tail("-h", "--help", "Show this message") do
33
+ CaseCheck.status_stream.puts opts
34
+ CaseCheck.exit
35
+ end
36
+
37
+ opts.on_tail("--version", "Show version") do
38
+ CaseCheck.status_stream.puts "#{name} #{CaseCheck.version}"
39
+ CaseCheck.exit
40
+ end
41
+ end.parse!(argv)
42
+
43
+ read_config!
44
+ end
45
+
46
+ def source_directory
47
+ @options.source_directory || '.'
48
+ end
49
+
50
+ def configuration_file
51
+ @options.configfile || File.join(source_directory, "cf_case_check.yml")
52
+ end
53
+
54
+ def coldfusion_directory
55
+ @options.coldfusion_directory || '/opt/coldfusion8'
56
+ end
57
+
58
+ def coldfusion_directory_given
59
+ @options.coldfusion_directory
60
+ end
61
+
62
+ def verbose?
63
+ @options.verbose
64
+ end
65
+
66
+ private
67
+
68
+ def read_config!
69
+ @configuration =
70
+ if coldfusion_directory_given && File.exist?(coldfusion_directory)
71
+ Coldfusion8Configuration.new(coldfusion_directory)
72
+ elsif File.exist?(configuration_file)
73
+ Configuration.new(configuration_file)
74
+ end
75
+ end
76
+ end
77
+
78
+ class Checker
79
+ def initialize(params)
80
+ @params = params
81
+ CaseCheck.status_stream.print "Reading source files "
82
+ @sources = extensions.collect {
83
+ |ext| [ext, Dir.glob("#{params.source_directory}/**/*.#{ext}", File::FNM_CASEFOLD)]
84
+ }.collect do |ext, files|
85
+ CaseCheck.status_stream.print "#{ext}: " unless files.empty?
86
+ files.collect do |f|
87
+ CaseCheck.status_stream.print '.'
88
+ ColdfusionSource.create(f)
89
+ end
90
+ end.flatten
91
+ CaseCheck.status_stream.puts
92
+ CaseCheck.status_stream.print "Analyzing "
93
+ @sources.each do |s|
94
+ CaseCheck.status_stream.print '.'
95
+ s.analyze
96
+ end
97
+ CaseCheck.status_stream.puts "\n"
98
+ end
99
+
100
+ def sources
101
+ @sources.reject { |src| @params.verbose? ? src.internal_references.empty? : src.inexact_internal_references.empty? }.
102
+ collect { |src| FilteredSource.new(src, @params.verbose?) }
103
+ end
104
+
105
+ def reference_count
106
+ sources.inject(0) { |c, s| c + s.internal_references.size }
107
+ end
108
+
109
+ def extensions
110
+ %w(cfm cfc tem)
111
+ end
112
+ end
113
+
114
+ class FilteredSource
115
+ attr_reader :src
116
+
117
+ def initialize(source, include_exact_matches)
118
+ @src = source
119
+ @include_exact = include_exact_matches
120
+ end
121
+
122
+ def internal_references
123
+ @include_exact ? src.internal_references : src.inexact_internal_references
124
+ end
125
+ end
126
+
127
+ end
@@ -0,0 +1,51 @@
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
+ private
14
+
15
+ def apply
16
+ read_custom_tag_dirs
17
+ read_cfc_dirs
18
+ read_substitutions
19
+ end
20
+
21
+ def read_custom_tag_dirs
22
+ CustomTag.directories = absolutize_directories(@doc['custom_tag_directories'] || [])
23
+ end
24
+
25
+ def read_cfc_dirs
26
+ Cfc.directories = absolutize_directories(@doc['cfc_directories'] || [])
27
+ end
28
+
29
+ def read_substitutions
30
+ if @doc['substitutions']
31
+ @doc['substitutions'].each_pair do |re, repl|
32
+ CaseCheck::Reference.substitutions << [Regexp.new(re, Regexp::IGNORECASE), repl]
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def absolutize_directories(dirs)
40
+ dirs.to_a.collect { |d|
41
+ p = Pathname.new(d)
42
+ if p.absolute?
43
+ p
44
+ else
45
+ Pathname.new(File.dirname(@filename)) + p
46
+ end
47
+ }.collect { |p| p.to_s }
48
+ end
49
+ end
50
+
51
+ 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,78 @@
1
+ # Models of various sorts of file references within CF code
2
+
3
+ require 'rubygems'
4
+ require 'activesupport'
5
+
6
+ module CaseCheck
7
+
8
+ # abstract base class
9
+ class Reference < Struct.new(:source, :text, :line)
10
+ class << self
11
+ def substitutions
12
+ @substitutions ||= []
13
+ end
14
+ end
15
+
16
+ # abstract methods
17
+ # - expected_path
18
+ # returns the exact relative path to which this reference refers
19
+ # - resolved_to
20
+ # returns the absolute file to which this reference seems to point, if one could be found
21
+
22
+ # Returns :exact, :case_insensitive, or nil depending on whether
23
+ # the reference could be resolved on a case_sensitive FS,
24
+ # only on a case_insensitive FS, or not at all
25
+ def resolution
26
+ return nil unless resolved_to
27
+ case_sensitive_match? ? :exact : :case_insensitive
28
+ end
29
+
30
+ def message
31
+ start =
32
+ case resolution
33
+ when :exact
34
+ "Exactly resolved"
35
+ when :case_insensitive
36
+ "Case-insensitively resolved"
37
+ else
38
+ "Unresolved"
39
+ end
40
+ msg = "#{start} #{type_name} on line #{line}"
41
+ if resolution
42
+ "#{msg} from #{text} to #{resolved_to}"
43
+ else
44
+ "#{msg}: #{text}"
45
+ end
46
+ end
47
+
48
+ def type_name
49
+ self.class.name.split('::').last.underscore.gsub('_', ' ')
50
+ end
51
+
52
+ def substituted_text
53
+ re, sub = CaseCheck::Reference.substitutions.detect { |expr, _| expr =~ text }
54
+ if re
55
+ text.sub(re, sub)
56
+ else
57
+ text
58
+ end
59
+ end
60
+
61
+ protected
62
+
63
+ def case_sensitive_match?
64
+ resolved_to.ends_with?(expected_path_tail)
65
+ end
66
+
67
+ def expected_path_tail
68
+ expected_path.split('/').reject { |pe| pe == '.' }.join('/').gsub(%r((\.\./)+), '')
69
+ end
70
+
71
+ def resolve_in(dir)
72
+ File.case_insensitive_canonical_name(File.expand_path(expected_path, dir))
73
+ end
74
+ end
75
+
76
+ end # module CaseCheck
77
+
78
+ CaseCheck.require_all_libs_relative_to(__FILE__, 'references')
@@ -0,0 +1,51 @@
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 = substituted_text.gsub('.', '/') + ".cfc"
28
+ @resolved_to = search_path.inject(nil) do |resolved, dir|
29
+ resolved || resolve_in(dir)
30
+ end
31
+ end
32
+
33
+ protected
34
+
35
+ def search_path
36
+ [File.dirname(source.filename)] + self.class.directories
37
+ end
38
+
39
+ def case_sensitive_match?
40
+ return true if super
41
+ # According to the CF docs:
42
+ # "On UNIX systems, ColdFusion searches first for a file with a name
43
+ # that matches the specified component name, but is all lowercase.
44
+ # If it does not find the file, it looks for a filename that matches
45
+ # the component name exactly, with the identical character casing."
46
+ resolved_to.ends_with?(expected_path_tail.downcase)
47
+ end
48
+
49
+ end
50
+
51
+ 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 = substituted_text
16
+ @resolved_to = resolve_in(File.dirname(source.filename))
17
+ end
18
+ end
19
+
20
+ end