# encoding: ascii-8bit# frozen_string_literal: truemoduleParsermoduleSource### A buffer with source code. {Buffer} contains the source code itself,# associated location information (name and first line), and takes care# of encoding.## A source buffer is immutable once populated.## @!attribute [r] name# Buffer name. If the buffer was created from a file, the name corresponds# to relative path to the file.# @return [String] buffer name## @!attribute [r] first_line# First line of the buffer, 1 by default.# @return [Integer] first line## @api public#classBufferattr_reader:name,:first_line### @api private#ENCODING_RE=/[\s#](en)?coding\s*[:=]\s*
(
# Special-case: there's a UTF8-MAC encoding.
(utf8-mac)
|
# Chew the suffix; it's there for emacs compat.
([A-Za-z0-9_-]+?)(-unix|-dos|-mac)
|
([A-Za-z0-9_-]+)
)
/x### Try to recognize encoding of `string` as Ruby would, i.e. by looking for# magic encoding comment or UTF-8 BOM. `string` can be in any encoding.## @param [String] string# @return [String, nil] encoding name, if recognized#defself.recognize_encoding(string)returnifstring.empty?# extract the first two lines in an efficient waystring=~/\A(.*)\n?(.*\n)?/first_line,second_line=$1,$2iffirst_line.start_with?("\xef\xbb\xbf".freeze)# BOMreturnEncoding::UTF_8elsiffirst_line[0,2]=='#!'.freezeencoding_line=second_lineelseencoding_line=first_lineendreturnnilifencoding_line.nil?||encoding_line[0]!='#'if(result=ENCODING_RE.match(encoding_line))Encoding.find(result[3]||result[4]||result[6])elsenilendend### Recognize encoding of `input` and process it so it could be lexed.## * If `input` does not contain BOM or magic encoding comment, it is# kept in the original encoding.# * If the detected encoding is binary, `input` is kept in binary.# * Otherwise, `input` is re-encoded into UTF-8 and returned as a# new string.## This method mutates the encoding of `input`, but not its content.## @param [String] input# @return [String]# @raise [EncodingError]#defself.reencode_string(input)original_encoding=input.encodingdetected_encoding=recognize_encoding(input.force_encoding(Encoding::BINARY))ifdetected_encoding.nil?input.force_encoding(original_encoding)elsifdetected_encoding==Encoding::BINARYinputelseinput.force_encoding(detected_encoding).encode(Encoding::UTF_8)endenddefinitialize(name,first_line=1,source: nil)@name=name.to_s@source=nil@first_line=first_line@lines=nil@line_begins=nil# UTF-32-reencoded source for O(1) slicing@slice_source=nil# Cache for fast lookup@line_index_for_position={}self.source=sourceifsourceend### Populate this buffer from correspondingly named file.## @example# Parser::Source::Buffer.new('foo/bar.rb').read## @return [Buffer] self# @raise [ArgumentError] if already populated#defreadFile.open(@name,'rb')do|io|self.source=io.readendselfend### Source code contained in this buffer.## @return [String] source code# @raise [RuntimeError] if buffer is not populated yet#defsourceif@source.nil?raiseRuntimeError,'Cannot extract source from uninitialized Source::Buffer'end@sourceend### Populate this buffer from a string with encoding autodetection.# `input` is mutated if not frozen.## @param [String] input# @raise [ArgumentError] if already populated# @raise [EncodingError] if `input` includes invalid byte sequence for the encoding# @return [String]#defsource=(input)input=input.dupifinput.frozen?input=self.class.reencode_string(input)unlessinput.valid_encoding?raiseEncodingError,"invalid byte sequence in #{input.encoding.name}"endself.raw_source=inputend### Populate this buffer from a string without encoding autodetection.## @param [String] input# @raise [ArgumentError] if already populated# @return [String]#defraw_source=(input)if@sourceraiseArgumentError,'Source::Buffer is immutable'end@source=input.gsub("\r\n".freeze,"\n".freeze).freezeif!@source.ascii_only?&&@source.encoding!=Encoding::UTF_32LE&&@source.encoding!=Encoding::BINARY@slice_source=@source.encode(Encoding::UTF_32LE)endenddefslice(start,length=nil)iflength.nil?ifstart.is_a?(::Range)length=start.sizestart=start.beginelselength=1endendif@slice_source.nil?@source[start,length]else@slice_source[start,length].encode(@source.encoding)endend### Convert a character index into the source to a `[line, column]` tuple.## @param [Integer] position# @return [[Integer, Integer]] `[line, column]`#defdecompose_position(position)line_index=line_index_for_position(position)line_begin=line_begins[line_index][@first_line+line_index,position-line_begin]end### Convert a character index into the source to a line number.## @param [Integer] position# @return [Integer] line# @api private#defline_for_position(position)line_index_for_position(position)+@first_lineend### Convert a character index into the source to a column number.## @param [Integer] position# @return [Integer] column# @api private#defcolumn_for_position(position)line_index=line_index_for_position(position)position-line_begins[line_index]end### Return an `Array` of source code lines.## @return [Array<String>]#defsource_lines@lines||=beginlines=@source.lines.to_alines<<''.dupif@source.end_with?("\n".freeze)lines.eachdo|line|line.chomp!("\n".freeze)line.freezeendlines.freezeendend### Extract line `lineno` from source, taking `first_line` into account.## @param [Integer] lineno# @return [String]# @raise [IndexError] if `lineno` is out of bounds#defsource_line(lineno)source_lines.fetch(lineno-@first_line).dupend### Extract line `lineno` as a new `Range`, taking `first_line` into account.## @param [Integer] lineno# @return [Range]# @raise [IndexError] if `lineno` is out of bounds#defline_range(lineno)index=lineno-@first_lineifindex<0||index+1>=line_begins.sizeraiseIndexError,'Parser::Source::Buffer: range for line '\"#{lineno} requested, valid line numbers are #{@first_line}.."\"#{@first_line+line_begins.size-2}"elseRange.new(self,line_begins[index],line_begins[index+1]-1)endend### @return [Range] A range covering the whole source#defsource_range@source_range||=Range.new(self,0,source.size)end### Number of last line in the buffer## @return [Integer]#deflast_lineline_begins.size+@first_line-2end# :nodoc:deffreezesource_lines;line_begins;source_range# build cachesuperend# :nodoc:definspect"#<#{self.class}#{name}>"endprivate# @returns [0, line_begin_of_line_1, ..., source.size + 1]defline_begins@line_begins||=beginbegins=[0]index=0whileindex=@source.index("\n".freeze,index)index+=1begins<<indexendbegins<<@source.size+1beginsendend# @returns 0-based line index of positiondefline_index_for_position(position)@line_index_for_position[position]||beginindex=bsearch(line_begins,position)-1@line_index_for_position[position]=indexunless@line_index_for_position.frozen?indexendendifArray.method_defined?(:bsearch_index)# RUBY_VERSION >= 2.3defbsearch(line_begins,position)line_begins.bsearch_indexdo|line_begin|position<line_beginend||line_begins.size-1# || only for out of bound valuesendelsedefbsearch(line_begins,position)@line_range||=0...line_begins.size@line_range.bsearchdo|i|position<line_begins[i]end||line_begins.size-1# || only for out of bound valuesendendendendend