Class: FeatureMap::Private::AssignmentMappers::FileAnnotations

Inherits:
Object
  • Object
show all
Includes:
Mapper
Defined in:
lib/feature_map/private/assignment_mappers/file_annotations.rb

Overview

Calculate, cache, and return a mapping of file names (relative to the root of the repository) to a feature name.

Example:

{
  'app/models/company.rb' => Feature.find('Onboarding'),
  ...
}

Constant Summary collapse

SINGLE_LINE_ANNOTATION_PATTERN =

NOTE: regex ‘x’ arg ignores whitespace within the construction of the regex.

regex 'm' arg allows the regex to _execute_ on multiline strings.
/
  \s* # Any amount of whitespace
  (#{Constants::SINGLE_LINE_COMMENT_PATTERNS.join('|')}) # Single line comment start
  \s* # Any amount of whitespace, not including newlines
  @feature\s # We find the feature annotation followed by one space
  (?<feature>.*?$) # A named capture grabs the rest as the feature until the line ends
/x.freeze
MULTILINE_ANNOTATION_PATTERN =
/
  (?:#{Constants::MULTILINE_COMMENT_START_PATTERNS.join('|')}) # Any comment start
  .*? # Followed by any characters, including newlines, until...
  @feature\s # We find the feature annotation followed by one space
  (?<feature>.*?$) # A named capture grabs the rest as the feature until the line ends
/xm.freeze
DESCRIPTION =
'Annotations at the top of file'

Instance Method Summary collapse

Methods included from Mapper

all, included, to_glob_cache

Instance Method Details

#bust_caches!Object



130
# File 'lib/feature_map/private/assignment_mappers/file_annotations.rb', line 130

def bust_caches!; end

#descriptionObject



126
127
128
# File 'lib/feature_map/private/assignment_mappers/file_annotations.rb', line 126

def description
  DESCRIPTION
end

#file_annotation_based_feature(filename) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/feature_map/private/assignment_mappers/file_annotations.rb', line 88

def file_annotation_based_feature(filename)
  # Not too sure what the following comment means but it was carried over from the code_ownership repo, so
  # I've opted to leave it unchanged in case it is helpful for future engineers:
  #   > If for a directory is named with an ownable extension, we need to skip
  #   > so File.foreach doesn't blow up below. This was needed because Cypress
  #   > screenshots are saved to a folder with the test suite filename.
  return if File.directory?(filename)
  return unless File.file?(filename)

  # The annotation should be on one of the first ten lines.
  # If the annotation isn't in the first ten lines we assume it
  # doesn't exist.

  lines = File.foreach(filename).first(10)
  return if lines.empty?

  feature = identify_feature_from(lines)
  return unless feature

  Private.find_feature!(
    feature,
    filename
  )
end

#globs_to_feature(files) ⇒ Object



41
42
43
44
45
46
47
48
# File 'lib/feature_map/private/assignment_mappers/file_annotations.rb', line 41

def globs_to_feature(files)
  files.each_with_object({}) do |filename_relative_to_root, mapping|
    feature = file_annotation_based_feature(filename_relative_to_root)
    next unless feature

    mapping[filename_relative_to_root] = feature
  end
end

#identify_feature_from(lines) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/feature_map/private/assignment_mappers/file_annotations.rb', line 73

def identify_feature_from(lines)
  matched_single_line_feature = lines.join("\n").match(SINGLE_LINE_ANNOTATION_PATTERN)
  matched_multiline_feature = lines.join("\n").match(MULTILINE_ANNOTATION_PATTERN)
  matched_feature = matched_single_line_feature || matched_multiline_feature
  return if matched_feature.nil?

  matched_feature
             .values_at(:feature)
             .first
             .gsub(/#{Constants::MULTILINE_COMMENT_END_PATTERNS.join('|')}/, '')
             .strip
rescue ArgumentError => e
  raise unless e.message.include?('invalid byte sequence')
end

#map_file_to_feature(file) ⇒ Object



37
38
39
# File 'lib/feature_map/private/assignment_mappers/file_annotations.rb', line 37

def map_file_to_feature(file)
  file_annotation_based_feature(file)
end

#remove_file_annotation!(filename) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/feature_map/private/assignment_mappers/file_annotations.rb', line 113

def remove_file_annotation!(filename)
  if file_annotation_based_feature(filename)
    filepath = Pathname.new(filename)
    lines = filepath.read.split("\n")
    new_lines = lines.reject { |line| line[SINGLE_LINE_ANNOTATION_PATTERN] }
    # We explicitly add a final new line since splitting by new line when reading the file lines
    # ignores new lines at the ends of files
    # We also remove leading new lines, since there is after a new line after an annotation
    new_file_contents = "#{new_lines.join("\n")}\n".gsub(/\A\n+/, '')
    filepath.write(new_file_contents)
  end
end

#update_cache(cache, files) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/feature_map/private/assignment_mappers/file_annotations.rb', line 50

def update_cache(cache, files)
  # We map files to nil features so that files whose annotation have been removed will be properly
  # overwritten (i.e. removed) from the cache.
  fileset = Set.new(files)
  updated_cache_for_files = globs_to_feature(files)
  cache.merge!(updated_cache_for_files)

  invalid_files = cache.keys.select do |file|
    # If a file is not tracked, it should be removed from the cache
    !Private.file_tracked?(file) ||
      # If a file no longer has a file annotation (i.e. `globs_to_feature` doesn't map it)
      # it should be removed from the cache
      # We make sure to only apply this to the input files since otherwise `updated_cache_for_files.key?(file)` would always return `false` when files == []
      (fileset.include?(file) && !updated_cache_for_files.key?(file))
  end

  invalid_files.each do |invalid_file|
    cache.delete(invalid_file)
  end

  cache
end