Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
name: CI
on: [push, pull_request]
jobs:
test:
rspec:
name: RSpec (Ruby ${{ matrix.ruby }})
strategy:
fail-fast: false
matrix:
ruby: ['2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
ruby: ['4.0', '3.4', '3.3', '3.2', '3.1', '3.0', '2.7', '2.6', '2.5']
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec rake
- name: Run RSpec
run: bundle exec rake spec

rubocop:
name: RuboCop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: '4.0'
bundler-cache: true
- name: Run RuboCop
run: bundle exec rake rubocop

rbs:
name: RBS
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: '4.0'
bundler-cache: true
- name: Validate RBS signatures
run: bundle exec rake rbs:validate
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ spec/reports
test/tmp
test/version_tmp
tmp
bin/
bin/

# RBS
.gem_rbs_collection
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

[Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v1.4.0...HEAD

## [1.4.0] - 2026-01-15

### Added
- RBS type signatures for improved type checking and IDE support ([#68])

### Changed
- Minor fixups in gem metadata ([#67]).

[Unreleased]: https://github.com/envato/zxcvbn-ruby/compare/v1.3.0...HEAD
[1.4.0]: https://github.com/envato/zxcvbn-ruby/compare/v1.3.0...v1.4.0
[#67]: https://github.com/envato/zxcvbn-ruby/pull/67
[#68]: https://github.com/envato/zxcvbn-ruby/pull/68

## [1.3.0] - 2026-01-02

Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ group :development do
gem 'guard-bundler', require: false
gem 'guard-rspec', require: false
gem 'rake'
gem 'rbs'
end

group :test do
Expand Down
30 changes: 29 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,35 @@ require 'rubocop/rake_task'
RSpec::Core::RakeTask.new('spec')
RuboCop::RakeTask.new('rubocop')

task default: %i[spec rubocop]
begin
require 'rbs/cli'

namespace :rbs do
desc 'Validate RBS type signatures'
task :validate do
sh 'rbs -I sig validate'
end

desc 'Run RBS runtime type checker with RSpec'
task :test do
sh "rbs -I sig test --target 'Zxcvbn::*' rspec"
end

desc 'List RBS types'
task :list do
sh 'rbs -I sig list | grep Zxcvbn'
end

desc 'Check RBS syntax'
task :parse do
sh 'rbs -I sig parse sig/**/*.rbs'
end
end
rescue LoadError
# RBS is not available
end

task default: %i[spec rubocop rbs:validate]

task :console do
require 'zxcvbn'
Expand Down
2 changes: 1 addition & 1 deletion lib/zxcvbn/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Zxcvbn
VERSION = '1.3.0'
VERSION = '1.4.0'
end
11 changes: 11 additions & 0 deletions rbs_collection.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# RBS collection configuration
# This file specifies which type signature repositories to use

sources:
- type: git
name: ruby/gem_rbs_collection
remote: https://github.com/ruby/gem_rbs_collection.git
revision: main
repo_dir: gems

path: .gem_rbs_collection
65 changes: 65 additions & 0 deletions sig/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# RBS Type Signatures

This directory contains [RBS](https://github.com/ruby/rbs) type signatures for the zxcvbn-ruby gem.

## What is RBS?

RBS is Ruby's type signature language. It provides a way to describe the structure of Ruby programs with:
- Class and module definitions
- Method signatures with parameter and return types
- Instance variables and constants
- Duck typing and union types

## Usage

### Validating Type Signatures

To validate that the RBS files are syntactically correct:

```bash
bundle exec rake rbs:validate
```

### Runtime Type Checking

To run runtime type checking against the actual Ruby code during tests:

```bash
bundle exec rake rbs:test
```

This runs the RSpec test suite with RBS type checking enabled, verifying that method calls match their type signatures at runtime. Note: This takes about 2 minutes to run.

### Other Useful Commands

List all Zxcvbn types:
```bash
bundle exec rake rbs:list
```

Check syntax of RBS files:
```bash
bundle exec rake rbs:parse
```

## File Structure

The signatures mirror the structure of the `lib/` directory:

- `sig/zxcvbn.rbs` - Main Zxcvbn module
- `sig/zxcvbn/*.rbs` - Core classes (Tester, Score, Match, etc.)
- `sig/zxcvbn/matchers/*.rbs` - Pattern matcher classes

## Adding New Signatures

When adding new classes or methods to the codebase, remember to:

1. Create or update the corresponding `.rbs` file in the `sig/` directory
2. Run `bundle exec rake rbs_validate` to ensure the syntax is correct
3. Keep type signatures in sync with the actual implementation

## Resources

- [RBS Documentation](https://github.com/ruby/rbs)
- [RBS Syntax Guide](https://github.com/ruby/rbs/blob/master/docs/syntax.md)
- [Ruby Signature Collection](https://github.com/ruby/gem_rbs_collection)
10 changes: 10 additions & 0 deletions sig/zxcvbn.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Zxcvbn
VERSION: String

DATA_PATH: Pathname

type word_list = Hash[String, Array[String]]
type user_inputs = Array[String]

def self.test: (String? password, ?user_inputs user_inputs, ?word_list word_lists) -> Score
end
13 changes: 13 additions & 0 deletions sig/zxcvbn/crack_time.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Zxcvbn
module CrackTime
SINGLE_GUESS: Float
NUM_ATTACKERS: Integer
SECONDS_PER_GUESS: Float

def entropy_to_crack_time: (Numeric entropy) -> Float

def crack_time_to_score: (Numeric seconds) -> Integer

def display_time: (Numeric seconds) -> String
end
end
31 changes: 31 additions & 0 deletions sig/zxcvbn/data.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Zxcvbn
class Data
type ranked_dictionary = Hash[String, Integer]
type adjacency_graph = Hash[String, Array[String?]]
type graph_stats = Hash[String, { average_degree: Float, starting_positions: Integer }]

@ranked_dictionaries: Hash[String, ranked_dictionary]
@adjacency_graphs: Hash[String, adjacency_graph]
@dictionary_tries: Hash[String, Trie]
@graph_stats: graph_stats

attr_reader ranked_dictionaries: Hash[String, ranked_dictionary]
attr_reader adjacency_graphs: Hash[String, adjacency_graph]
attr_reader dictionary_tries: Hash[String, Trie]
attr_reader graph_stats: graph_stats

def initialize: () -> void

def add_word_list: (String name, Array[String] list) -> void

private

def read_word_list: (String file) -> Array[String]

def build_tries: () -> Hash[String, Trie]

def build_trie: (ranked_dictionary ranked_dictionary) -> Trie

def compute_graph_stats: () -> graph_stats
end
end
7 changes: 7 additions & 0 deletions sig/zxcvbn/dictionary_ranker.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Zxcvbn
class DictionaryRanker
def self.rank_dictionaries: (Hash[String | Symbol, Array[String]] lists) -> Hash[String | Symbol, Hash[String, Integer]]

def self.rank_dictionary: (Array[String] words) -> Hash[String, Integer]
end
end
33 changes: 33 additions & 0 deletions sig/zxcvbn/entropy.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Zxcvbn
module Entropy
include Zxcvbn::Math

def calc_entropy: (Match match) -> Float

def repeat_entropy: (Match match) -> Float

def sequence_entropy: (Match match) -> Float

def digits_entropy: (Match match) -> Float

def year_entropy: (Match? match) -> Float

def date_entropy: (Match match) -> Float

def dictionary_entropy: (Match match) -> Float

def extra_uppercase_entropy: (Match match) -> Numeric

def extra_l33t_entropy: (Match match) -> Numeric

def spatial_entropy: (Match match) -> Float

NUM_YEARS: Integer
NUM_MONTHS: Integer
NUM_DAYS: Integer
START_UPPER: Regexp
END_UPPER: Regexp
ALL_UPPER: Regexp
ALL_LOWER: Regexp
end
end
8 changes: 8 additions & 0 deletions sig/zxcvbn/feedback.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Zxcvbn
class Feedback
attr_accessor warning: String?
attr_accessor suggestions: Array[String]

def initialize: (?warning: String?, ?suggestions: Array[String]) -> void
end
end
13 changes: 13 additions & 0 deletions sig/zxcvbn/feedback_giver.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Zxcvbn
class FeedbackGiver
NAME_DICTIONARIES: Array[String]
DEFAULT_FEEDBACK: Feedback
EMPTY_FEEDBACK: Feedback

def self.get_feedback: (Integer? score, Array[Match] sequence) -> Feedback

def self.get_match_feedback: (Match match, bool is_sole_match) -> Feedback?

def self.get_dictionary_match_feedback: (Match match, bool is_sole_match) -> Feedback
end
end
38 changes: 38 additions & 0 deletions sig/zxcvbn/match.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module Zxcvbn
class Match
attr_accessor pattern: String?
attr_accessor i: Integer?
attr_accessor j: Integer?
attr_accessor token: String?
attr_accessor matched_word: String?
attr_accessor rank: Integer?
attr_accessor dictionary_name: String?
attr_accessor reversed: bool?
attr_accessor l33t: bool?
attr_accessor sub: Hash[String, String]?
attr_accessor sub_display: String?
attr_accessor l: Integer?
attr_accessor entropy: Numeric?
attr_accessor base_entropy: Numeric?
attr_accessor uppercase_entropy: Numeric?
attr_accessor l33t_entropy: Numeric?
attr_accessor repeated_char: String?
attr_accessor sequence_name: String?
attr_accessor sequence_space: Integer?
attr_accessor ascending: bool?
attr_accessor graph: String?
attr_accessor turns: Integer?
attr_accessor shifted_count: Integer?
attr_accessor shiffted_count: Integer?
attr_accessor year: Integer?
attr_accessor month: Integer?
attr_accessor day: Integer?
attr_accessor separator: String?
attr_accessor cardinality: Integer?
attr_accessor offset: Integer?

def initialize: (**untyped attributes) -> void

def to_hash: () -> Hash[String, untyped]
end
end
13 changes: 13 additions & 0 deletions sig/zxcvbn/math.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Zxcvbn
module Math
def bruteforce_cardinality: (String password) -> Integer

def lg: (Numeric n) -> Float

def nCk: (Integer n, Integer k) -> Integer

def average_degree_for_graph: (String graph_name) -> Float

def starting_positions_for_graph: (String graph_name) -> Integer
end
end
Loading