# frozen_string_literal: truerequire"json"require"openssl"require"ariadne/view_components/constants"require_relative"tag_tree_helpers"# :nocov:moduleERBLintmoduleLinters# Provides the basic linter logic. When inherited, you should define:# * `TAGS` - required - The HTML tags that the component supports. It will be used by the linter to match elements.# * `MESSAGE` - required - The message shown when there's an offense.# * `CLASSES` - optional - The CSS classes that the component needs. The linter will only match elements with one of those classes.# * `REQUIRED_ARGUMENTS` - optional - A list of HTML attributes that are required by the component.classBaseLinter<LinterincludeTagTreeHelpersDUMP_FILE=".erblint-counter-ignore.json"DISALLOWED_CLASSES=[].freezeCLASSES=[].freezeREQUIRED_ARGUMENTS=[].freezeclassConfigSchema<LinterConfig# rubocop:disable Style/Documentationproperty:override_ignores_if_correctable,accepts: [true,false],default: false,reader: :override_ignores_if_correctable?endclass<<selfdefinherited(base)superbase.include(ERBLint::LinterRegistry)base.config_schema=ConfigSchemaendenddefrun(processed_source)@total_offenses=0@offenses_not_corrected=0(tags,tag_tree)=build_tag_tree(processed_source)tags.eachdo|tag|nextiftag.closing?nextifself.class::TAGS&.none?(tag.name)classes=tag.attributes["class"]&.value&.split(" ")||[]tag_tree[tag][:offense]=falsenextifclasses.intersect?(self.class::DISALLOWED_CLASSES)nextunlessself.class::CLASSES.blank?||classes.intersect?(self.class::CLASSES)args=map_arguments(tag,tag_tree[tag])correction=correction(args)attributes=tag.attributes.each.map(&:name).join(" ")matches_required_attributes=self.class::REQUIRED_ARGUMENTS.blank?||self.class::REQUIRED_ARGUMENTS.all?{|arg|attributes.match?(arg)}tag_tree[tag][:offense]=truetag_tree[tag][:correctable]=matches_required_attributes&&!correction.nil?tag_tree[tag][:message]=message(args,processed_source)tag_tree[tag][:correction]=correctionendtag_tree.eachdo|tag,h|nextunlessh[:offense]@total_offenses+=1# We always fix the offenses using blocks. The closing tag corresponds to `<% end %>`.ifh[:correctable]add_correction(tag,h)else@offenses_not_corrected+=1generate_offense(self.class,processed_source,tag,h[:message])endendcounter_correct?(processed_source)dump_data(processed_source)ifENV["DUMP_LINT_DATA"]=="1"enddefautocorrect(processed_source,offense)returnunlessoffense.contextlambdado|corrector|ifoffense.context.include?(counter_disable)correct_counter(corrector,processed_source,offense)elsecorrector.replace(offense.source_range,offense.context)endendendprivatedefadd_correction(tag,tag_tree)add_offense(tag.loc,tag_tree[:message],tag_tree[:correction])add_offense(tag_tree[:closing].loc,tag_tree[:message],"<% end %>")end# Override this function to convert the HTML element attributes to argument for a component.## @return [Hash] if possible to map all attributes to arguments.# @return [Nil] if cannot map to arguments.defmap_arguments(_tag,_tag_tree)nilend# Override this function to define how to autocorrect an element to a component.## @return [String] with the text to replace the HTML element if possible to correct.# @return [Nil] if cannot correct element.defcorrection(_tag)nilend# Override this function to customize the linter message.## @return [String] message to show on linter error.defmessage(_tag,_processed_source)self.class::MESSAGEenddefcounter_disable"erblint:counter #{self.class.name.demodulize}"enddefcorrect_counter(corrector,processed_source,offense)ifprocessed_source.file_content.include?(counter_disable)# update the counter if existscorrector.replace(offense.source_range,offense.context)else# add comment with counter if nonecorrector.insert_before(processed_source.source_buffer.source_range,"#{offense.context}\n")endenddeftags(processed_source)processed_source.parser.nodes_with_type(:tag).map{|tag_node|BetterHtml::Tree::Tag.from_node(tag_node)}enddefcounter_correct?(processed_source)comment_node=nilexpected_count=0rule_name=self.class.name.match(/:?:?(\w+)\Z/)[1]processed_source.parser.ast.descendants(:erb).eachdo|node|indicator_node,_,code_node,=*nodeindicator=indicator_node&.loc&.sourcecomment=code_node&.loc&.source&.stripifindicator=="#"&&comment.start_with?("erblint:count")&&comment.match(rule_name)comment_node=nodeexpected_count=comment.match(/\s(\d+)\s?$/)[1].to_iendend# Unless explicitly set, we don't want to mark correctable offenses if the counter is correct.if!@config.override_ignores_if_correctable?&&expected_count==@total_offensesclear_offensesreturnendif@offenses_not_corrected.zero?# have to adjust to get `\n` so we delete the whole lineadd_offense(processed_source.to_source_range(comment_node.loc.adjust(end_pos: 1)),"Unused erblint:count comment for #{rule_name}","")ifcomment_nodereturnendfirst_offense=@offenses[0]ifcomment_node.nil?add_offense(processed_source.to_source_range(first_offense.source_range),"#{rule_name}: If you must, add <%# erblint:counter #{rule_name}#{@offenses_not_corrected} %> to bypass this check.","<%# erblint:counter #{rule_name}#{@offenses_not_corrected} %>")elsifexpected_count!=@offenses_not_correctedadd_offense(processed_source.to_source_range(comment_node.loc),"Incorrect erblint:counter number for #{rule_name}. Expected: #{expected_count}, actual: #{@offenses_not_corrected}.","<%# erblint:counter #{rule_name}#{@offenses_not_corrected} %>")# the only offenses remaining are not autocorrectable, so we can ignore themelsifexpected_count==@offenses_not_corrected&&@offenses.size==@offenses_not_correctedclear_offensesendenddefgenerate_offense(klass,processed_source,tag,message=nil,replacement=nil)message||=klass::MESSAGEklass_name=klass.name.demodulizeoffense=["#{klass_name}:#{message}",tag.node.loc.source].join("\n")add_offense(processed_source.to_source_range(tag.loc),offense,replacement)enddefdump_data(processed_source)returnif@total_offenses.zero?data=File.exist?(DUMP_FILE)?JSON.parse(File.read(DUMP_FILE)):{}data[processed_source.filename]||={}data[processed_source.filename][self.class.name.demodulize]=@total_offensesFile.write(DUMP_FILE,JSON.pretty_generate(data))endendendend