Summary
Replace the String#include? substring test in Example#generate with Ant-style glob matching via File.fnmatch so that YardExampleTest.skip patterns are precise and unambiguous.
Motivation
The current skip check uses substring matching:
# lib/yard_example_test/example.rb
next if YardExampleTest.skips.any? { |skip| this.definition.include?(skip) }
Because YARD paths such as "FooBar#baz" contain shorter names as substrings, a pattern like "Foo" — intended to skip only the Foo class — will also silently skip FooBar#baz, Foo::Bar#baz, and any other path that happens to contain "Foo".
Concrete example
YardExampleTest.skip('Foo') # intended: skip Foo only
# Paths that are INCORRECTLY skipped:
# FooBar#baz — starts with "Foo"
# Foo::Bar.baz — "Foo" is a prefix component
# SomeClass#get_foo — "foo" embedded in method name (case-sensitive, but still fragile)
There is also no way to express "skip all methods of Foo but not Foo itself", or "skip only the Foo class but not its subnamespace".
Current behaviour
# lib/yard_example_test/example.rb
next if YardExampleTest.skips.any? { |skip| this.definition.include?(skip) }
this.definition is a YARD path string such as "MyClass#my_method" or "MyClass::Nested.class_method".
Proposed solution
Normalise :: → / in both the pattern and the definition, then delegate to File.fnmatch with File::FNM_PATHNAME:
next if YardExampleTest.skips.any? do |skip|
pattern = skip.gsub('::', '/')
definition = this.definition.gsub('::', '/')
File.fnmatch(pattern, definition, File::FNM_PATHNAME)
end
With FNM_PATHNAME, * matches any characters except /, so it naturally stops at namespace (::) and method separator (#, .) boundaries. ** matches across boundaries.
Pattern reference
| Pattern |
Matches |
Does not match |
"MyClass" |
MyClass |
MyClass#foo, FooMyClass |
"MyClass#*" |
MyClass#foo, MyClass#bar |
MyClass.foo, MyClass::Nested#foo |
"MyClass.*" |
MyClass.foo |
MyClass#foo |
"MyClass::*" |
MyClass::Nested |
MyClass, MyClass::Nested#foo |
"MyClass::**" |
MyClass, MyClass::Nested, MyClass::Nested::Deep |
MyClass#foo (method) |
"MyClass::**::*" |
MyClass::Nested, MyClass::Nested::Deep |
MyClass itself |
"MyClass::**::AnotherClass" |
MyClass::AnotherClass, MyClass::Nested::AnotherClass |
MyClass::SomethingElse |
"MyClass::**#*" |
MyClass#foo, MyClass::Nested#bar |
MyClass, MyClass::Nested |
Note: ** followed by a literal segment (e.g. "MyClass::**::AnotherClass") requires the definition to have that segment at any depth inside MyClass. Because File::FNM_PATHNAME requires ** to be its own path component, patterns must use :: separators correctly (e.g. MyClass::**::Something, not MyClass::**Something).
Scope
In scope
- Replace the
include? check in Example#generate with File.fnmatch-based matching
- Update
YardExampleTest.skip API docs to describe the new glob syntax and list example patterns
- Update the
generate YARD doc (step 2 of the numbered list) to reference glob semantics
- Unit/integration tests covering the pattern table above
Out of scope
- Changes to how skip patterns are stored (
Array unchanged)
- Case-insensitive matching
- Negation patterns (e.g.
!MyClass#foo)
Implementation notes
-
Normalisation — both skip and this.definition must have :: replaced with / before passing to File.fnmatch. The YARD separators # and . are left as-is; File::FNM_PATHNAME treats them as ordinary characters, so * will not cross them.
-
File::FNM_PATHNAME — without this flag, * matches / and the namespace isolation breaks down. Always pass this flag.
-
** semantics — File.fnmatch("MyClass/**", "MyClass/Nested/Deep", File::FNM_PATHNAME) returns true; File.fnmatch("MyClass/**", "MyClass", File::FNM_PATHNAME) returns false. A convenience helper definition_matches?(skip, definition) could abstract the normalisation.
-
Backward compatibility — existing plain-name patterns (e.g. "MyClass") already work correctly as exact matches under the new scheme, no migration needed for users who skip by full class name.
Acceptance criteria
TDD guidance
Starter tests (RSpec or Minitest)
# For each row: [skip_pattern, definition, should_skip?]
CASES = [
['Foo', 'Foo', true],
['Foo', 'FooBar', false],
['Foo', 'FooBar#baz', false],
['Foo', 'Foo#bar', false],
['Foo#*', 'Foo#bar', true],
['Foo#*', 'Foo#baz', true],
['Foo#*', 'Foo.bar', false],
['Foo#*', 'Foo::Nested#bar', false],
['Foo.*', 'Foo.bar', true],
['Foo.*', 'Foo#bar', false],
['Foo::*', 'Foo::Bar', true],
['Foo::*', 'Foo', false],
['Foo::*', 'Foo::Bar::Baz', false],
['Foo::**', 'Foo', true],
['Foo::**', 'Foo::Bar', true],
['Foo::**', 'Foo::Bar::Baz', true],
['Foo::**', 'Foo#bar', false],
['Foo::**::*', 'Foo::Bar', true],
['Foo::**::*', 'Foo', false],
['Foo::**#*', 'Foo#bar', true],
['Foo::**#*', 'Foo::Bar#baz', true],
['Foo::**#*', 'Foo', false],
].freeze
Branch & PR guidance
- Branch from
initial_implementation
- Suggested branch name:
refactor/skip-glob-matching
- PR title:
refactor(skip): use File.fnmatch for glob-based skip pattern matching
- Reference this issue in the PR description
Related
lib/yard_example_test/example.rb — Example#generate, line with include? check
lib/yard_example_test.rb — YardExampleTest.skip and YardExampleTest.skips API
Summary
Replace the
String#include?substring test inExample#generatewith Ant-style glob matching viaFile.fnmatchso thatYardExampleTest.skippatterns are precise and unambiguous.Motivation
The current skip check uses substring matching:
Because YARD paths such as
"FooBar#baz"contain shorter names as substrings, a pattern like"Foo"— intended to skip only theFooclass — will also silently skipFooBar#baz,Foo::Bar#baz, and any other path that happens to contain"Foo".Concrete example
There is also no way to express "skip all methods of
Foobut notFooitself", or "skip only theFooclass but not its subnamespace".Current behaviour
this.definitionis a YARD path string such as"MyClass#my_method"or"MyClass::Nested.class_method".Proposed solution
Normalise
::→/in both the pattern and the definition, then delegate toFile.fnmatchwithFile::FNM_PATHNAME:With
FNM_PATHNAME,*matches any characters except/, so it naturally stops at namespace (::) and method separator (#,.) boundaries.**matches across boundaries.Pattern reference
"MyClass"MyClassMyClass#foo,FooMyClass"MyClass#*"MyClass#foo,MyClass#barMyClass.foo,MyClass::Nested#foo"MyClass.*"MyClass.fooMyClass#foo"MyClass::*"MyClass::NestedMyClass,MyClass::Nested#foo"MyClass::**"MyClass,MyClass::Nested,MyClass::Nested::DeepMyClass#foo(method)"MyClass::**::*"MyClass::Nested,MyClass::Nested::DeepMyClassitself"MyClass::**::AnotherClass"MyClass::AnotherClass,MyClass::Nested::AnotherClassMyClass::SomethingElse"MyClass::**#*"MyClass#foo,MyClass::Nested#barMyClass,MyClass::NestedScope
In scope
include?check inExample#generatewithFile.fnmatch-based matchingYardExampleTest.skipAPI docs to describe the new glob syntax and list example patternsgenerateYARD doc (step 2 of the numbered list) to reference glob semanticsOut of scope
Arrayunchanged)!MyClass#foo)Implementation notes
Normalisation — both
skipandthis.definitionmust have::replaced with/before passing toFile.fnmatch. The YARD separators#and.are left as-is;File::FNM_PATHNAMEtreats them as ordinary characters, so*will not cross them.File::FNM_PATHNAME— without this flag,*matches/and the namespace isolation breaks down. Always pass this flag.**semantics —File.fnmatch("MyClass/**", "MyClass/Nested/Deep", File::FNM_PATHNAME)returnstrue;File.fnmatch("MyClass/**", "MyClass", File::FNM_PATHNAME)returnsfalse. A convenience helperdefinition_matches?(skip, definition)could abstract the normalisation.Backward compatibility — existing plain-name patterns (e.g.
"MyClass") already work correctly as exact matches under the new scheme, no migration needed for users who skip by full class name.Acceptance criteria
skip("Foo")no longer matchesFooBar#bazskip("Foo")still matchesFooexactlyskip("Foo#*")skips all instance methods ofFoobut notFooitselfskip("Foo::*")skips direct namespace children ofFoobut notFooitself or deeper descendantsskip("Foo::**")skipsFooand all descendants (classes and modules, not methods)skip("Foo::**#*")skips all instance methods at any depth underFooYardExampleTest.skipYARD doc lists the supported glob syntaxTDD guidance
Starter tests (RSpec or Minitest)
Branch & PR guidance
initial_implementationrefactor/skip-glob-matchingrefactor(skip): use File.fnmatch for glob-based skip pattern matchingRelated
lib/yard_example_test/example.rb—Example#generate, line withinclude?checklib/yard_example_test.rb—YardExampleTest.skipandYardExampleTest.skipsAPI