# frozen_string_literal: truerequire'pathname'moduleRuboCopmoduleCopmoduleNaming# Makes sure that Ruby source files have snake_case# names. Ruby scripts (i.e. source files with a shebang in the# first line) are ignored.## The cop also ignores `.gemspec` files, because Bundler# recommends using dashes to separate namespaces in nested gems# (i.e. `bundler-console` becomes `Bundler::Console`). As such, the# gemspec is supposed to be named `bundler-console.gemspec`.## When `ExpectMatchingDefinition` (default: `false`) is `true`, the cop requires# each file to have a class, module or `Struct` defined in it that matches# the filename. This can be further configured using# `CheckDefinitionPathHierarchy` (default: `true`) to determine whether the# path should match the namespace of the above definition.## When `IgnoreExecutableScripts` (default: `true`) is `true`, files that start# with a shebang line are not considered by the cop.## When `Regex` is set, the cop will flag any filename that does not match# the regular expression.## @example# # bad# lib/layoutManager.rb## anything/usingCamelCase## # good# lib/layout_manager.rb## anything/using_snake_case.rakeclassFileName<BaseincludeRangeHelpMSG_SNAKE_CASE='The name of this source file (`%<basename>s`) should use snake_case.'MSG_NO_DEFINITION='`%<basename>s` should define a class or module called `%<namespace>s`.'MSG_REGEX='`%<basename>s` should match `%<regex>s`.'SNAKE_CASE=/^[\d[[:lower:]]_.?!]+$/.freeze# @!method struct_definition(node)def_node_matcher:struct_definition,<<~PATTERN
{
(casgn $_ $_ (send (const {nil? cbase} :Struct) :new ...))
(casgn $_ $_ (block (send (const {nil? cbase} :Struct) :new ...) ...))
}
PATTERNdefon_new_investigationfile_path=processed_source.file_pathreturnifconfig.file_to_exclude?(file_path)||config.allowed_camel_case_file?(file_path)for_bad_filename(file_path){|range,msg|add_offense(range,message: msg)}endprivatedeffor_bad_filename(file_path)basename=File.basename(file_path)iffilename_good?(basename)msg=perform_class_and_module_naming_checks(file_path,basename)elsemsg=other_message(basename)unlessbad_filename_allowed?endyieldsource_range(processed_source.buffer,1,0),msgifmsgenddefperform_class_and_module_naming_checks(file_path,basename)returnunlessexpect_matching_definition?ifcheck_definition_path_hierarchy?&&!matching_definition?(file_path)msg=no_definition_message(basename,file_path)elsif!matching_class?(basename)msg=no_definition_message(basename,basename)endmsgenddefmatching_definition?(file_path)find_class_or_module(processed_source.ast,to_namespace(file_path))enddefmatching_class?(file_name)find_class_or_module(processed_source.ast,to_namespace(file_name))enddefbad_filename_allowed?ignore_executable_scripts?&&processed_source.start_with?('#!')enddefno_definition_message(basename,file_path)format(MSG_NO_DEFINITION,basename: basename,namespace: to_namespace(file_path).join('::'))enddefother_message(basename)ifregexformat(MSG_REGEX,basename: basename,regex: regex)elseformat(MSG_SNAKE_CASE,basename: basename)endenddefignore_executable_scripts?cop_config['IgnoreExecutableScripts']enddefexpect_matching_definition?cop_config['ExpectMatchingDefinition']enddefcheck_definition_path_hierarchy?cop_config['CheckDefinitionPathHierarchy']enddefdefinition_path_hierarchy_rootscop_config['CheckDefinitionPathHierarchyRoots']||[]enddefregexcop_config['Regex']enddefallowed_acronymscop_config['AllowedAcronyms']||[]enddeffilename_good?(basename)basename=basename.sub(/^\./,'')basename=basename.sub(/\.[^.]+$/,'')# special handling for Action Pack Variants file names like# some_file.xlsx+mobile.axlsxbasename=basename.sub('+','_')basename.match?(regex||SNAKE_CASE)enddeffind_class_or_module(node,namespace)returnnilunlessnodename=namespace.popon_node(%i[class module casgn],node)do|child|nextunless(const=find_definition(child))const_namespace,const_name=*constnextifname!=const_name&&!match_acronym?(name,const_name)nextunlessnamespace.empty?||match_namespace(child,const_namespace,namespace)returnnodeendnilenddeffind_definition(node)node.defined_module||defined_struct(node)enddefdefined_struct(node)namespace,name=*struct_definition(node)s(:const,namespace,name)ifnameenddefmatch_namespace(node,namespace,expected)match_partial=partial_matcher!(expected)match_partial.call(namespace)node.each_ancestor(:class,:module,:sclass,:casgn)do|ancestor|returnfalseifancestor.sclass_type?match_partial.call(ancestor.defined_module)endmatch?(expected)enddefpartial_matcher!(expected)lambdado|namespace|whilenamespacereturnmatch?(expected)ifnamespace.cbase_type?namespace,name=*namespaceexpected.popifname==expected.last||match_acronym?(expected.last,name)endfalseendenddefmatch?(expected)expected.empty?||expected==[:Object]enddefmatch_acronym?(expected,name)expected=expected.to_sname=name.to_sallowed_acronyms.any?{|acronym|expected.gsub(acronym.capitalize,acronym)==name}enddefto_namespace(path)# rubocop:disable Metrics/AbcSizecomponents=Pathname(path).each_filename.to_a# To convert a pathname to a Ruby namespace, we need a starting point# But RC can be run from any working directory, and can check any path# We can't assume that the working directory, or any other, is the# "starting point" to build a namespace.start=definition_path_hierarchy_rootsstart_index=nil# To find the closest namespace root take the path components, and# then work through them backwards until we find a candidate. This# makes sure we work from the actual root in the case of a path like# /home/user/src/project_name/lib.components.reverse.each_with_indexdo|c,i|ifstart.include?(c)start_index=components.size-ibreakendendifstart_index.nil?[to_module_name(components.last)]elsecomponents[start_index..].map{|c|to_module_name(c)}endenddefto_module_name(basename)words=basename.sub(/\..*/,'').split('_')words.map(&:capitalize).join.to_symendendendendend