Class: Audition::Static::SourceFile
- Inherits:
-
Object
- Object
- Audition::Static::SourceFile
- Defined in:
- lib/audition/static/source_file.rb
Overview
A parsed Ruby file plus the magic comments that change Ractor semantics.
Constant Summary collapse
- CONST_MUTATORS =
Method names that read as in-place data mutation when sent to a constant. Shared by the mutable-constants check and the fixers: a constant this file mutates is a deliberate accumulator (sinatra's PARAMS_CONFIG) and must never be frozen by magic comment or wrap.
%i[ []= << push unshift concat merge! replace ].freeze
- INDEX_WRITES =
Index writes are their own node types, not calls:
COUNTS[k] += 1is IndexOperatorWriteNode andCACHE[k] ||= vis IndexOrWriteNode; missing them let a.freezeautofix produce FrozenError at runtime. [ Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexTargetNode ].freeze
- MAGIC_KEYS =
Keys Ruby actually honors. Prism reports every comment shaped like
# key: value, which sweeps up documentation (# I18n.t: 'date.formats.short'); inserting after those would land a magic comment mid-file. %w[ encoding coding frozen_string_literal shareable_constant_value warn_indent ].freeze
Instance Attribute Summary collapse
-
#parse_result ⇒ Object
readonly
Returns the value of attribute parse_result.
-
#path ⇒ Object
readonly
Returns the value of attribute path.
-
#source ⇒ Object
readonly
Returns the value of attribute source.
Class Method Summary collapse
Instance Method Summary collapse
-
#boot_insertion ⇒ Object
Where a hoisted
requireline can be inserted: right after the last existing top-level require, or after the leading comment block (shebang and magic comments), or at the top. - #first_statement_offset ⇒ Object
-
#frozen_string_literal? ⇒ Boolean
String literals in this file are frozen (and therefore shareable when they contain no interpolation).
-
#initialize(source:, path:) ⇒ SourceFile
constructor
A new instance of SourceFile.
- #leading_comments_end ⇒ Object
- #line_at(number) ⇒ Object
-
#magic_comment(key) ⇒ Object
Ruby matches magic-comment keys and values case-insensitively and ignores magic comments that appear after the first statement (verified on 4.0); Prism reports them all, so both rules are applied here.
-
#magic_insertion_offset ⇒ Object
Where a new magic comment can go: after the shebang and any existing magic comments, before code.
-
#mutated_constants ⇒ Array<String>
Names of constants that receive a mutator call somewhere in this file.
-
#raw ⇒ Object
Prism reports byte offsets; index math against the source must run on a binary copy or multibyte content shifts every computed position.
- #root ⇒ Object
-
#shareable_constants? ⇒ Boolean
# shareable_constant_value: literal|experimental_everything| experimental_copymakes constant values deeply frozen and shareable at parse time, so constant checks are moot. - #syntax_errors ⇒ Object
-
#top_level_requires ⇒ Object
Literal feature strings required by top-level statements.
- #valid_syntax? ⇒ Boolean
Constructor Details
#initialize(source:, path:) ⇒ SourceFile
Returns a new instance of SourceFile.
16 17 18 19 20 |
# File 'lib/audition/static/source_file.rb', line 16 def initialize(source:, path:) @source = source @path = path @parse_result = Prism.parse(source) end |
Instance Attribute Details
#parse_result ⇒ Object (readonly)
Returns the value of attribute parse_result.
10 11 12 |
# File 'lib/audition/static/source_file.rb', line 10 def parse_result @parse_result end |
#path ⇒ Object (readonly)
Returns the value of attribute path.
10 11 12 |
# File 'lib/audition/static/source_file.rb', line 10 def path @path end |
#source ⇒ Object (readonly)
Returns the value of attribute source.
10 11 12 |
# File 'lib/audition/static/source_file.rb', line 10 def source @source end |
Class Method Details
.read(path) ⇒ Object
12 13 14 |
# File 'lib/audition/static/source_file.rb', line 12 def self.read(path) new(source: File.read(path), path: path) end |
Instance Method Details
#boot_insertion ⇒ Object
Where a hoisted require line can be inserted: right after
the last existing top-level require, or after the leading
comment block (shebang and magic comments), or at the top.
82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/audition/static/source_file.rb', line 82 def boot_insertion last_require = root.statements.body.rfind do |s| s.is_a?(Prism::CallNode) && s.receiver.nil? && %i[require require_relative].include?(s.name) end if last_require newline = raw.index("\n", last_require.location.end_offset) offset = newline ? newline + 1 : raw.bytesize {offset: offset, after_require: true} else {offset: leading_comments_end, after_require: false} end end |
#first_statement_offset ⇒ Object
41 42 43 44 |
# File 'lib/audition/static/source_file.rb', line 41 def first_statement_offset first = root.statements.body.first first ? first.location.start_offset : source.bytesize end |
#frozen_string_literal? ⇒ Boolean
String literals in this file are frozen (and therefore shareable when they contain no interpolation).
48 49 50 |
# File 'lib/audition/static/source_file.rb', line 48 def frozen_string_literal? magic_comment("frozen_string_literal") == "true" end |
#leading_comments_end ⇒ Object
179 180 181 182 183 184 185 186 187 |
# File 'lib/audition/static/source_file.rb', line 179 def leading_comments_end offset = 0 source.each_line do |line| break unless line.start_with?("#") offset += line.bytesize end offset end |
#line_at(number) ⇒ Object
61 62 63 64 65 66 |
# File 'lib/audition/static/source_file.rb', line 61 def line_at(number) @lines ||= source.lines.map do |line| line.dup.force_encoding(Encoding::UTF_8).scrub end @lines[number - 1]&.strip end |
#magic_comment(key) ⇒ Object
Ruby matches magic-comment keys and values case-insensitively and ignores magic comments that appear after the first statement (verified on 4.0); Prism reports them all, so both rules are applied here.
32 33 34 35 36 37 38 39 |
# File 'lib/audition/static/source_file.rb', line 32 def magic_comment(key) limit = first_statement_offset comment = parse_result.magic_comments.find do |mc| mc.key_loc.slice.downcase == key && mc.key_loc.start_offset < limit end comment&.value_loc&.slice&.downcase end |
#magic_insertion_offset ⇒ Object
Where a new magic comment can go: after the shebang and any existing magic comments, before code.
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/audition/static/source_file.rb', line 162 def magic_insertion_offset offset = 0 if raw.start_with?("#!") newline = raw.index("\n") offset = newline ? newline + 1 : raw.bytesize end limit = first_statement_offset after_magic = parse_result.magic_comments.filter_map do |mc| next unless MAGIC_KEYS.include?(mc.key_loc.slice.downcase) next unless mc.key_loc.start_offset < limit newline = raw.index("\n", mc.value_loc.end_offset) newline ? newline + 1 : raw.bytesize end.max [offset, after_magic || 0].max end |
#mutated_constants ⇒ Array<String>
Returns names of constants that receive a mutator call somewhere in this file.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/audition/static/source_file.rb', line 125 def mutated_constants @mutated_constants ||= begin names = [] queue = [root] until queue.empty? node = queue.shift queue.concat(node.child_nodes.compact) receiver = if node.is_a?(Prism::CallNode) && CONST_MUTATORS.include?(node.name) node.receiver elsif INDEX_WRITES.any? { |type| node.is_a?(type) } node.receiver end case receiver when Prism::ConstantReadNode names << receiver.name.to_s when Prism::ConstantPathNode names << receiver.location.slice end end names.uniq end end |
#raw ⇒ Object
Prism reports byte offsets; index math against the source must run on a binary copy or multibyte content shifts every computed position.
99 100 101 |
# File 'lib/audition/static/source_file.rb', line 99 def raw @raw ||= source.dup.force_encoding(Encoding::BINARY) end |
#root ⇒ Object
26 |
# File 'lib/audition/static/source_file.rb', line 26 def root = parse_result.value |
#shareable_constants? ⇒ Boolean
# shareable_constant_value: literal|experimental_everything| experimental_copy makes constant values deeply frozen and
shareable at parse time, so constant checks are moot.
(v1 treats the comment as file-wide.)
56 57 58 59 |
# File 'lib/audition/static/source_file.rb', line 56 def shareable_constants? value = magic_comment("shareable_constant_value") !value.nil? && value != "none" end |
#syntax_errors ⇒ Object
24 |
# File 'lib/audition/static/source_file.rb', line 24 def syntax_errors = parse_result.errors |
#top_level_requires ⇒ Object
Literal feature strings required by top-level statements.
69 70 71 72 73 74 75 76 77 |
# File 'lib/audition/static/source_file.rb', line 69 def top_level_requires @top_level_requires ||= root.statements.body.filter_map do |s| next unless s.is_a?(Prism::CallNode) && s.receiver.nil? next unless %i[require require_relative].include?(s.name) arg = s.arguments&.arguments&.first arg.unescaped if arg.is_a?(Prism::StringNode) end end |
#valid_syntax? ⇒ Boolean
22 |
# File 'lib/audition/static/source_file.rb', line 22 def valid_syntax? = parse_result.success? |