NAME

Test::Spelling::Stopwords - POD spell-checking with project-specific stopwords

VERSION

Version 0.02

SYNOPSIS

Minimal - just drop this into your xt/ directory:

# xt/spell-pod.t
use Test::More;
use Test::Spelling::Stopwords;

unless ($ENV{AUTHOR_TESTING} || $ENV{RELEASE_TESTING} || $ENV{CI}) {
    plan skip_all => 'Spelling tests only run under AUTHOR_TESTING';
}

all_pod_files_spelling_ok();

Or with explicit configuration:

use Test::Spelling::Stopwords;

set_spell_lang('en_US');
set_stopwords_file('xt/.stopwords');
set_spell_dirs('lib', 'bin');

all_pod_files_spelling_ok();

Or with per-call overrides:

all_pod_files_spelling_ok(
    lang           => 'en_US',
    stopwords_file => 'xt/.stopwords',
    dirs           => ['lib', 'bin'],
);

Check a single file:

use Test::Spelling::Stopwords;
use Test::More tests => 1;

pod_file_spelling_ok('lib/My/Module.pm');

DESCRIPTION

Test::Spelling::Stopwords is a drop-in POD spell-checker that integrates project-specific stopword files with aspell. It is designed to work alongside the companion gen-stopwords script, which auto-generates a .stopwords file containing only the vocabulary unique to your project - after filtering out the common Perl ecosystem terms already covered by Pod::Wordlist.

How it differs from Test::Spelling

Test::Spelling is the established CPAN module for POD spell-checking. Test::Spelling::Stopwords does not replace it - it complements it by addressing two specific gaps:

Automatic stopwords file discovery

Test::Spelling requires you to call add_stopwords() explicitly or maintain a __DATA__ section in your test. Test::Spelling::Stopwords automatically discovers and loads a .stopwords file from your project root (or any path you configure), so your test file contains no project-specific content and can be reused across projects unchanged.

Line-number reporting

When Test::Spelling finds a misspelled word it tells you the word but not where it is. Test::Spelling::Stopwords reports the exact line number(s) in the source file where each misspelling appears, making failures fast to locate and fix.

Two-layer stopword architecture

The module merges two sources of known words before checking any file:

Layer 1 - Pod::Wordlist

The CPAN-maintained vocabulary of common Perl and technical terms (ok, undef, dbi, CPAN, accessor, mutators, etc.). This mirrors what gen-stopwords filters out when building .stopwords, so the module and the generator always agree on what counts as a known word.

Without this layer the test is stricter than the generator and flags words that Pod::Wordlist covers - causing false failures even on a freshly generated .stopwords.

Layer 2 - .stopwords

Project-specific vocabulary generated by gen-stopwords. Contains only terms not already covered by Pod::Wordlist.

Stopwords file format

The .stopwords file is a plain text file with one word per line. Lines beginning with # and blank lines are ignored.

# Auto-generated stopwords for en_GB
dbic
mojolicious
resultset
myauthor

Generate it with the companion gen-stopwords script:

gen-stopwords --dir lib --dir bin

Freshness check

On every run, Test::Spelling::Stopwords compares the modification time of your .stopwords file against your source files. If any source file is newer, it emits a diag warning:

# ------------------------------------------------------------
# WARNING: .stopwords is out of date!
# Run gen-stopwords to regenerate it.
# ------------------------------------------------------------

This is advisory only - the test continues to run.

POD cleaning

Before passing each line to aspell, all POD formatting codes are stripped entirely:

E<gt>           removed  (not 'gt', preventing the 'Egt' artefact)
L<Some::Module> removed
C<code>         removed
B<bold>         removed

This is more aggressive than simple content extraction and prevents a class of false positives caused by POD entity fragments appearing as bare words.

Environment variables

All defaults can be overridden without editing the test file:

SPELL_LANG

Aspell language code. Default: en_GB.

STOPWORD_FILE

Path to the stopwords file. Default: .stopwords.

SPELL_DIRS

Colon- or comma-separated list of directories to scan. Default: lib:bin:script.

ASPELL_CMD

Complete aspell command string, including all flags. Default: aspell list -l $LANG --run-together.

CONFIGURATION API

set_spell_lang

set_spell_lang('en_US');

Sets the aspell language code. May also be set via the SPELL_LANG environment variable.

set_stopwords_file

set_stopwords_file('xt/.stopwords');

Sets the path to the stopwords file. May also be set via the STOPWORD_FILE environment variable.

set_spell_dirs

set_spell_dirs('lib', 'bin', 'script');
set_spell_dirs( ['lib', 'bin'] );

Sets the list of directories to search for POD files. Accepts either a list or an arrayref. May also be set via the SPELL_DIRS environment variable.

get_stopwords_file

my $path = get_stopwords_file();

Returns the currently configured stopwords file path.

EXPORTED FUNCTIONS

all_pod_files_spelling_ok

all_pod_files_spelling_ok();

all_pod_files_spelling_ok(
    lang           => 'en_US',
    stopwords_file => 'xt/.stopwords',
    dirs           => ['lib', 'bin'],
);

Finds all Perl source files (.pm, .pl, .pod, .t) under the configured source directories, and runs a spell-check on the POD in each one. Emits one TAP pass/fail per file.

Misspelled words are reported via diag with their line numbers:

not ok 1 - POD spelling: lib/My/Module.pm
#   'serialiisable'  line(s): 42
#   'Egtconnect'     line(s): 17, 83

Accepts an optional hash of per-call overrides (lang, stopwords_file, dirs) that take precedence over the module-level configuration for the duration of the call.

Skips gracefully (via skip_all) if:

  • aspell is not installed or not on $PATH

  • The stopwords file does not exist

  • No POD files are found in the configured directories

pod_file_spelling_ok

pod_file_spelling_ok($file);
pod_file_spelling_ok($file, \%stopwords);
pod_file_spelling_ok($file, \%stopwords, $test_name);

Spell-checks the POD in a single file. Emits one pass or fail.

If \%stopwords is omitted the configured stopwords file is loaded automatically. $test_name defaults to "POD spelling: $file".

Returns true if the file passes, false otherwise.

RECOMMENDED WORKFLOW

  1. Install dependencies:

    cpanm -vS Test::Spelling::Stopwords

    This also installs the companion gen-stopwords script.

  2. Generate your project's stopwords file:

    gen-stopwords --dir lib --dir bin

    This scans your source files, runs aspell, filters out terms already covered by Pod::Wordlist, and writes a lean .stopwords containing only project-specific vocabulary.

  3. Create xt/spell-pod.t:

    use Test::More;
    use Test::Spelling::Stopwords;
    
    unless ($ENV{AUTHOR_TESTING} || $ENV{RELEASE_TESTING} || $ENV{CI}) {
        plan skip_all => 'Spelling tests only run under AUTHOR_TESTING';
    }
    
    all_pod_files_spelling_ok();
  4. Run:

    AUTHOR_TESTING=1 prove -lv xt/spell-pod.t
  5. After adding or editing source files, regenerate:

    gen-stopwords

    The test will warn you if you forget.

DEPENDENCIES

BUGS AND LIMITATIONS

  • aspell must be installed externally. The module skips gracefully if it is absent but cannot install it for you.

  • The shell pipe to aspell (via backticks) means Windows is not currently supported. Patches welcome.

  • The freshness check uses file modification times, which are reset by git checkout and similar operations. It is advisory only.

SEE ALSO

AUTHOR

Mohammad Sajid Anwar <mohammad.anwar@yahoo.com>

REPOSITORY

https://github.com/manwar/Test-Spelling-Stopwords

BUGS

Please report any bugs or feature requests through the web interface at https://github.com/manwar/Test-Spelling-Stopwords/issues. I will be notified and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc Test::Spelling::Stopwords

You can also look for information at:

LICENSE AND COPYRIGHT

Copyright (C) 2026 Mohammad Sajid Anwar.

This program is free software; you can redistribute it and / or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at: http://www.perlfoundation.org/artistic_license_2_0 Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License.By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license. If your Modified Version has been derived from a Modified Version made by someone other than you,you are nevertheless required to ensure that your Modified Version complies with the requirements of this license. This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder. This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement,then this Artistic License to you shall terminate on the date that such litigation is filed. Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.