├── .github └── FUNDING.yml ├── .gitignore ├── .rspec ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib └── rake │ ├── notes.rb │ └── notes │ ├── rake_task.rb │ ├── source_annotation_extractor.rb │ └── version.rb ├── rake-notes.gemspec └── spec ├── rake └── notes │ └── source_annotation_extractor_spec.rb └── spec_helper.rb /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # # These are supported funding model platforms 2 | # 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | # polar: # Replace with a single Polar username 13 | buy_me_a_coffee: "benjaminoakes" 14 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | InstalledFiles 7 | _yardoc 8 | coverage 9 | doc/ 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | *.swp 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .tmp 19 | vendor/bundle 20 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in rake-notes.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rake-notes (1.0.0) 5 | colored 6 | rake 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | colored (1.2) 12 | diff-lcs (1.5.1) 13 | rake (13.2.1) 14 | rspec (3.13.0) 15 | rspec-core (~> 3.13.0) 16 | rspec-expectations (~> 3.13.0) 17 | rspec-mocks (~> 3.13.0) 18 | rspec-core (3.13.0) 19 | rspec-support (~> 3.13.0) 20 | rspec-expectations (3.13.0) 21 | diff-lcs (>= 1.2.0, < 2.0) 22 | rspec-support (~> 3.13.0) 23 | rspec-mocks (3.13.1) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.13.0) 26 | rspec-support (3.13.1) 27 | 28 | PLATFORMS 29 | ruby 30 | x86_64-linux 31 | 32 | DEPENDENCIES 33 | rake-notes! 34 | rspec 35 | 36 | BUNDLED WITH 37 | 2.5.9 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Fabio Rehm 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rake-notes 2 | 3 | `rake notes` task for non-Rails' projects (heavily based on Rails' one ;) 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | gem 'rake-notes' 10 | 11 | And then execute: 12 | 13 | $ bundle 14 | 15 | Or install it yourself as: 16 | 17 | $ gem install rake-notes 18 | 19 | And add this line to your project's Rakefile: 20 | 21 | require 'rake/notes/rake_task' 22 | 23 | 24 | ## Acknowledgement 25 | 26 | Special thanks to everyone that contributed to the original 27 | [Rails' code](https://github.com/rails/rails/blob/master/railties/lib/rails/source_annotation_extractor.rb) 28 | 29 | 30 | ## Contributing 31 | 32 | 1. Fork it 33 | 2. Create your feature branch (`git checkout -b my-new-feature`) 34 | 3. Commit your changes (`git commit -am 'Add some feature'`) 35 | 4. Push to the branch (`git push origin my-new-feature`) 36 | 5. Create new Pull Request 37 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new(:test) 5 | 6 | task :default => [:test] 7 | -------------------------------------------------------------------------------- /lib/rake/notes.rb: -------------------------------------------------------------------------------- 1 | require 'rake/notes/version' 2 | 3 | # Nothing to see here, see rake_task.rb 4 | -------------------------------------------------------------------------------- /lib/rake/notes/rake_task.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/tasklib' 3 | 4 | require 'rake/notes/source_annotation_extractor' 5 | 6 | module Rake 7 | module Notes 8 | class RakeTask < ::Rake::TaskLib 9 | include ::Rake::DSL if defined?(::Rake::DSL) 10 | 11 | def initialize(*args) 12 | yield self if block_given? 13 | 14 | desc "Enumerate all annotations (use notes:optimize, :fixme, :todo for focus)" 15 | task :notes do 16 | SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", :tag => true 17 | end 18 | 19 | namespace :notes do 20 | ["OPTIMIZE", "FIXME", "TODO"].each do |annotation| 21 | desc "Enumerate all #{annotation} annotations" 22 | task annotation.downcase.intern do 23 | SourceAnnotationExtractor.enumerate annotation 24 | end 25 | end 26 | 27 | desc "Enumerate a custom annotation, specify with ANNOTATION=CUSTOM" 28 | task :custom do 29 | SourceAnnotationExtractor.enumerate ENV['ANNOTATION'] 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | 37 | Rake::Notes::RakeTask.new 38 | -------------------------------------------------------------------------------- /lib/rake/notes/source_annotation_extractor.rb: -------------------------------------------------------------------------------- 1 | require 'colored' 2 | require 'yaml' 3 | 4 | module Rake 5 | module Notes 6 | # From: 7 | # https://github.com/rails/rails/blob/master/railties/lib/rails/source_annotation_extractor.rb 8 | # 9 | # Implements the logic behind the rake tasks for annotations like 10 | # 11 | # rake notes 12 | # rake notes:optimize 13 | # 14 | # and friends. See rake -T notes. 15 | # 16 | # Annotation objects are triplets :line, :tag, :text that 17 | # represent the line where the annotation lives, its tag, and its text. Note 18 | # the filename is not stored. 19 | # 20 | # Annotations are looked for in comments and modulus whitespace they have to 21 | # start with the tag optionally followed by a colon. Everything up to the end 22 | # of the line (or closing ERB comment tag) is considered to be their text. 23 | class SourceAnnotationExtractor 24 | RUBYFILES = %w( Vagrantfile Rakefile Puppetfile Gemfile ) 25 | # `gem install --standalone` puts gems into $project/bundle and it really slows 26 | # down the rake task 27 | DEFAULT_IGNORE_DIRS = { 'ignore' => [ './bundle', './vendor' ] } 28 | 29 | def self.ignore_dirs 30 | @ignore_dirs ||= begin 31 | config = DEFAULT_IGNORE_DIRS 32 | config = config.merge(YAML.safe_load(File.read('./.rake-notes.yml'))) if File.exist?('./.rake-notes.yml') 33 | config['ignore'] || [] 34 | end 35 | end 36 | 37 | class Annotation < Struct.new(:line, :tag, :text) 38 | COLORS = { 39 | 'OPTIMIZE' => 'cyan', 40 | 'FIXME' => 'red', 41 | 'TODO' => 'yellow' 42 | } 43 | 44 | # Returns a representation of the annotation that looks like this: 45 | # 46 | # [126] [TODO] This algorithm is simple and clearly correct, make it faster. 47 | # 48 | # If +options+ has a flag :tag the tag is shown as in the example above. 49 | # Otherwise the string contains just line and text. 50 | def to_s(options={}) 51 | colored_tag = COLORS[tag.to_s].nil? ? tag : tag.send(COLORS[tag.to_s]) 52 | s = "[#{line.to_s.rjust(options[:indent]).green}] " 53 | s << "[#{colored_tag}] " if options[:tag] 54 | s << text 55 | end 56 | end 57 | 58 | # Prints all annotations with tag +tag+ under the current directory. Only 59 | # known file types are taken into account. The +options+ hash is passed 60 | # to each annotation's +to_s+. 61 | # 62 | # This class method is the single entry point for the rake tasks. 63 | def self.enumerate(tag, options={}) 64 | extractor = new(tag) 65 | extractor.display(extractor.find, options) 66 | end 67 | 68 | attr_reader :tag 69 | 70 | def initialize(tag) 71 | @tag = tag 72 | end 73 | 74 | # Returns a hash that maps filenames to arrays with their annotations. 75 | def find 76 | find_in('.') 77 | end 78 | 79 | # Returns a hash that maps filenames under +dir+ (recursively) to arrays 80 | # with their annotations. Only files with annotations are included, and only 81 | # known file types are taken into account. 82 | def find_in(dir) 83 | results = {} 84 | Dir.glob("#{dir}/*") do |item| 85 | next if File.basename(item)[0] == ?. 86 | 87 | if File.directory?(item) 88 | next if SourceAnnotationExtractor.ignore_dirs.include?(item) 89 | results.update(find_in(item)) 90 | elsif item =~ /\.(builder|rb|coffee|rake|pp|ya?ml|gemspec|feature)$/ || RUBYFILES.include?(File.basename(item)) 91 | results.update(extract_annotations_from(item, /#\s*(#{tag}):?\s*(.*)$/)) 92 | elsif item =~ /\.(css|scss|js|ts|tsx)$/ 93 | results.update(extract_annotations_from(item, /\/\/\s*(#{tag}):?\s*(.*)$/)) 94 | elsif item =~ /\.erb$/ 95 | results.update(extract_annotations_from(item, /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/)) 96 | elsif item =~ /\.haml$/ 97 | results.update(extract_annotations_from(item, /-\s*#\s*(#{tag}):?\s*(.*)$/)) 98 | elsif item =~ /\.slim$/ 99 | results.update(extract_annotations_from(item, /\/\s*\s*(#{tag}):?\s*(.*)$/)) 100 | elsif item =~ /\.md$/ 101 | results.update(extract_annotations_from(item, /\*\*\s*(#{tag})\**:?\s*(.*)\**$/)) 102 | end 103 | end 104 | 105 | results 106 | end 107 | 108 | # If +file+ is the filename of a file that contains annotations this method returns 109 | # a hash with a single entry that maps +file+ to an array of its annotations. 110 | # Otherwise it returns an empty hash. 111 | def extract_annotations_from(file, pattern) 112 | lineno = 0 113 | result = File.readlines(file).inject([]) do |list, line| 114 | lineno += 1 115 | next list unless line =~ pattern 116 | list << Annotation.new(lineno, $1, $2) 117 | end 118 | result.empty? ? {} : { file => result } 119 | end 120 | 121 | # Prints the mapping from filenames to annotations in +results+ ordered by filename. 122 | # The +options+ hash is passed to each annotation's +to_s+. 123 | def display(results, options={}) 124 | options[:indent] = results.map { |f, a| a.map(&:line) }.flatten.max.to_s.size 125 | out = options.delete(:out) || $stdout 126 | results.keys.sort.each do |file| 127 | out.puts "#{file[2..-1]}:" 128 | results[file].each do |note| 129 | out.puts " * #{note.to_s(options)}" 130 | end 131 | out.puts 132 | end 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/rake/notes/version.rb: -------------------------------------------------------------------------------- 1 | module Rake 2 | module Notes 3 | VERSION = "1.0.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /rake-notes.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rake/notes/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "rake-notes" 8 | gem.version = Rake::Notes::VERSION 9 | gem.authors = ["Fabio Rehm"] 10 | gem.email = ["fgrehm@gmail.com"] 11 | gem.description = "rake notes task for non-Rails' projects" 12 | gem.summary = "rake notes task for non-Rails' projects" 13 | gem.homepage = "https://github.com/fgrehm/rake-notes" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | gem.add_dependency 'rake' 21 | gem.add_dependency 'colored' 22 | 23 | gem.add_development_dependency 'rspec' 24 | end 25 | -------------------------------------------------------------------------------- /spec/rake/notes/source_annotation_extractor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'rake/notes/source_annotation_extractor' 4 | 5 | describe Rake::Notes::SourceAnnotationExtractor do 6 | before do 7 | @current_path = Dir.pwd 8 | Dir.mkdir(fixture_path) unless Dir.exist?(fixture_path) 9 | Dir.chdir(fixture_path) 10 | end 11 | 12 | after do 13 | Dir.chdir(@current_path) 14 | end 15 | 16 | context 'extracting notes based on file type' do 17 | subject do 18 | subj = StringIO.new 19 | described_class.enumerate('TODO', :out => subj) 20 | subj.string 21 | end 22 | 23 | before(:all) do 24 | fixture_file "index.html.erb", "<% # TODO: note in erb %>" 25 | fixture_file "index.html.haml", "- # TODO: note in haml" 26 | fixture_file "index.html.slim", "/ TODO: note in slim" 27 | fixture_file "application.js.coffee", "# TODO: note in coffee" 28 | fixture_file "application.js", "// TODO: note in js" 29 | fixture_file "application.css", "// TODO: note in css" 30 | fixture_file "application.css.scss", "// TODO: note in scss" 31 | fixture_file "component.tsx", "// TODO: note in tsx" 32 | fixture_file "application.ts", "// TODO note in ts" 33 | fixture_file "application_controller.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in ruby" 34 | fixture_file "task.rake", "# TODO: note in rake" 35 | fixture_file "init.pp", "# TODO: note in puppet" 36 | fixture_file "config.yml", "# TODO: note in yml" 37 | fixture_file "config.yaml", "# TODO: note in yaml" 38 | fixture_file "gem.gemspec", "# TODO: note in gemspec" 39 | fixture_file "Vagrantfile", "# TODO: note in vagrantfile" 40 | fixture_file "Rakefile", "# TODO: note in rakefile" 41 | fixture_file "Puppetfile", "# TODO: note in puppetfile" 42 | fixture_file "Gemfile", "# TODO: note in gemfile" 43 | fixture_file "feature.feature", "# TODO: note in cucumber feature" 44 | fixture_file "README.md", "**TODO**: note in markdown version one" 45 | fixture_file "CONTRIBUTING.md", "**TODO: note in markdown version two**" 46 | end 47 | 48 | it { should match(/note in erb/) } 49 | it { should match(/note in haml/) } 50 | it { should match(/note in slim/) } 51 | it { should match(/note in ruby/) } 52 | it { should match(/note in coffee/) } 53 | it { should match(/note in js/) } 54 | it { should match(/note in css/) } 55 | it { should match(/note in scss/) } 56 | it { should match(/note in tsx/) } 57 | it { should match(/note in ts/) } 58 | it { should match(/note in rake/) } 59 | it { should match(/note in puppet/) } 60 | it { should match(/note in yml/) } 61 | it { should match(/note in yaml/) } 62 | it { should match(/note in gemspec/) } 63 | it { should match(/note in vagrantfile/) } 64 | it { should match(/note in rakefile/) } 65 | it { should match(/note in puppetfile/) } 66 | it { should match(/note in gemfile/) } 67 | it { should match(/note in cucumber feature/) } 68 | it { should match(/note in markdown version one/) } 69 | it { should match(/note in markdown version two/) } 70 | end 71 | 72 | def fixture_file(path, contents) 73 | FileUtils.mkdir_p File.dirname("#{fixture_path}/#{path}") 74 | File.open("#{fixture_path}/#{path}", 'w') do |f| 75 | f.puts contents 76 | end 77 | end 78 | 79 | def fixture_path 80 | "#{Dir.pwd}/.tmp" 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | Bundler.require(:default) 3 | 4 | RSpec.configure do |config| 5 | config.treat_symbols_as_metadata_keys_with_true_values = true 6 | config.run_all_when_everything_filtered = true 7 | config.filter_run :focus 8 | 9 | # Run specs in random order to surface order dependencies. If you find an 10 | # order dependency and want to debug it, you can fix the order by providing 11 | # the seed, which is printed after each run. 12 | # --seed 1234 13 | config.order = 'random' 14 | end 15 | --------------------------------------------------------------------------------