├── .gitignore ├── .rbenv-gemsets ├── .ruby-version ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── lib ├── sqlint.rb └── sqlint │ ├── version.rb │ └── linter.rb ├── Rakefile ├── .pre-commit-hooks.yaml ├── Gemfile ├── Makefile ├── TODO ├── flake.lock ├── flake.nix ├── LICENSE.txt ├── sqlint.gemspec ├── Gemfile.lock ├── bin └── sqlint ├── README.md ├── spec └── linter_spec.rb └── gemset.nix /.gitignore: -------------------------------------------------------------------------------- 1 | /*.gem 2 | -------------------------------------------------------------------------------- /.rbenv-gemsets: -------------------------------------------------------------------------------- 1 | sqlint -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: sanityinc 2 | -------------------------------------------------------------------------------- /lib/sqlint.rb: -------------------------------------------------------------------------------- 1 | require 'sqlint/version' 2 | require 'sqlint/linter' 3 | -------------------------------------------------------------------------------- /lib/sqlint/version.rb: -------------------------------------------------------------------------------- 1 | module SQLint 2 | VERSION = "0.3.0" 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rspec/core/rake_task' 3 | RSpec::Core::RakeTask.new(:spec) 4 | rescue LoadError 5 | end 6 | 7 | task default: :spec 8 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: sqlint 3 | name: sqlint 4 | entry: sqlint 5 | language: ruby 6 | minimum_pre_commit_version: 0.15.0 7 | types: [sql] 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | ruby ">= 3.0" 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pg_query", ">= 1.0" 6 | 7 | group :development do 8 | gem "rspec" 9 | gem "rake" 10 | end 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | bundle exec rspec 3 | 4 | gem: 5 | # Ensure perms are correct 6 | chmod +x bin/sqlint 7 | chmod -R a+r * 8 | chmod -R a+X * 9 | gem build sqlint.gemspec 10 | 11 | .PHONY: gem test 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | commit-message: 9 | prefix: "chore" 10 | include: "scope" 11 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - [-] More input test cases? 2 | - [-] Handle any warnings returned by PgQuery.parse 3 | - [X] Limit number of warnings 4 | - [ ] Optionally connect to a live DB to check referenced table/column names are valid 5 | - [ ] Refactor and test the CLI front end 6 | - [ ] Support for other SQL dialects 7 | - [ ] JSON output 8 | - [ ] Huge files 9 | - [X] Maybe truncate long 'at or near "blah"' strings 10 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1752077645, 6 | "narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "be9e214982e20b8310878ac2baa063a961c1bdf6", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "nixpkgs", 14 | "ref": "nixpkgs-unstable", 15 | "type": "indirect" 16 | } 17 | }, 18 | "root": { 19 | "inputs": { 20 | "nixpkgs": "nixpkgs" 21 | } 22 | } 23 | }, 24 | "root": "root", 25 | "version": 7 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request, workflow_dispatch] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: [ubuntu-latest, macos-latest] 9 | # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0' 10 | ruby: ['3.0', '3.1', '3.2', '3.3'] 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - uses: actions/checkout@v6 14 | - uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: ${{ matrix.ruby }} 17 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 18 | - run: bundle exec rake 19 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "sqlint tool"; 3 | 4 | inputs = { 5 | nixpkgs.url = "nixpkgs/nixpkgs-unstable"; 6 | }; 7 | 8 | outputs = 9 | { self, nixpkgs }@inputs: 10 | let 11 | forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.platforms.all; 12 | in 13 | { 14 | devShell = forAllSystems ( 15 | system: 16 | let 17 | pkgs = import nixpkgs { inherit system; }; 18 | env = pkgs.bundlerEnv { 19 | name = "sqlint"; 20 | gemdir = ./.; 21 | groups = [ 22 | "default" 23 | "development" 24 | "test" 25 | ]; 26 | 27 | meta = with pkgs.lib; { 28 | description = "sqlint"; 29 | platforms = platforms.unix; 30 | }; 31 | }; 32 | in 33 | pkgs.mkShell { 34 | buildInputs = [ 35 | env 36 | pkgs.bundix 37 | ]; 38 | } 39 | ); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Powershop NZ Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /sqlint.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __FILE__) 4 | require 'sqlint' 5 | require 'English' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'sqlint' 9 | s.version = SQLint::VERSION 10 | s.platform = Gem::Platform::RUBY 11 | s.required_ruby_version = '>= 3.0' 12 | s.authors = ['Steve Purcell', 'Kieran Trezona-le Comte'] 13 | s.description = <<-EOF 14 | Simple SQL linter. 15 | EOF 16 | 17 | s.email = 'steve@sanityinc.com' 18 | s.files = `git ls-files`.split($RS).reject do |file| 19 | file =~ %r{^(?: 20 | spec/.* 21 | |Gemfile 22 | |Rakefile 23 | |\.rspec 24 | |\.ruby-version 25 | |\.rbenv-gemsets 26 | |\.gitignore 27 | |\.travis.yml 28 | )$}x 29 | end 30 | s.test_files = [] 31 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } 32 | s.extra_rdoc_files = ['LICENSE.txt', 'README.md'] 33 | s.homepage = 'https://github.com/purcell/sqlint' 34 | s.licenses = ['MIT'] 35 | s.require_paths = ['lib'] 36 | s.rubygems_version = '1.8.23' 37 | s.summary = 'Simple SQL linter.' 38 | 39 | s.add_runtime_dependency('pg_query', '>= 1') 40 | s.add_development_dependency('rake', '>= 12.3.3') 41 | s.add_development_dependency('rspec', '~> 3.2') 42 | s.add_development_dependency('bundler', '>= 2.2.33') 43 | end 44 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | bigdecimal (3.1.9) 5 | diff-lcs (1.6.1) 6 | google-protobuf (4.30.2) 7 | bigdecimal 8 | rake (>= 13) 9 | google-protobuf (4.30.2-aarch64-linux) 10 | bigdecimal 11 | rake (>= 13) 12 | google-protobuf (4.30.2-arm64-darwin) 13 | bigdecimal 14 | rake (>= 13) 15 | google-protobuf (4.30.2-x86-linux) 16 | bigdecimal 17 | rake (>= 13) 18 | google-protobuf (4.30.2-x86_64-darwin) 19 | bigdecimal 20 | rake (>= 13) 21 | google-protobuf (4.30.2-x86_64-linux) 22 | bigdecimal 23 | rake (>= 13) 24 | pg_query (6.1.0) 25 | google-protobuf (>= 3.25.3) 26 | rake (13.2.1) 27 | rspec (3.13.0) 28 | rspec-core (~> 3.13.0) 29 | rspec-expectations (~> 3.13.0) 30 | rspec-mocks (~> 3.13.0) 31 | rspec-core (3.13.3) 32 | rspec-support (~> 3.13.0) 33 | rspec-expectations (3.13.3) 34 | diff-lcs (>= 1.2.0, < 2.0) 35 | rspec-support (~> 3.13.0) 36 | rspec-mocks (3.13.2) 37 | diff-lcs (>= 1.2.0, < 2.0) 38 | rspec-support (~> 3.13.0) 39 | rspec-support (3.13.2) 40 | 41 | PLATFORMS 42 | aarch64-linux 43 | arm64-darwin 44 | ruby 45 | x86-linux 46 | x86_64-darwin 47 | x86_64-linux 48 | 49 | DEPENDENCIES 50 | pg_query (>= 1.0) 51 | rake 52 | rspec 53 | 54 | RUBY VERSION 55 | ruby 3.3.5p100 56 | 57 | BUNDLED WITH 58 | 2.5.16 59 | -------------------------------------------------------------------------------- /bin/sqlint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift(File.dirname(File.realpath(__FILE__)) + '/../lib') 4 | require 'pg_query' 5 | require 'sqlint' 6 | require 'optparse' 7 | 8 | options = { limit: 1000 } 9 | optparse = OptionParser.new do |opts| 10 | opts.banner = "Usage: #{File.basename($0)} [options] [file.sql ...]" 11 | opts.separator "" 12 | opts.separator "Options:" 13 | opts.on("--limit=N", Integer, "Limit checking to N errors (default: #{options[:limit]})") do |n| 14 | options[:limit] = n 15 | end 16 | opts.on_tail("-h", "--help", "Print this help") do 17 | puts opts 18 | exit 0 19 | end 20 | opts.on_tail("-v", "--version", "Display the version") do 21 | puts SQLint::VERSION 22 | exit 0 23 | end 24 | end 25 | optparse.parse!(ARGV) 26 | 27 | ERROR_TYPES = {error: "ERROR", warning: "WARNING"} 28 | 29 | class String 30 | def sanitise 31 | gsub(/[[:cntrl:]+]/) do |bad| 32 | bad.codepoints.map { |c| "\\%04d" % c }.join 33 | end 34 | end 35 | end 36 | 37 | def display_lint(lint) 38 | message_lines = lint.message.split("\n") 39 | puts [ 40 | lint.filename, 41 | lint.line, 42 | lint.column, 43 | ERROR_TYPES[lint.type] + " " + message_lines.shift.sanitise 44 | ].join(":") 45 | message_lines.each do |line| 46 | puts " " + line.sanitise 47 | end 48 | end 49 | 50 | def each_input_file(&block) 51 | if ARGV.empty? 52 | yield [STDIN, "stdin"] 53 | else 54 | ARGV.each do |filename| 55 | File.open(filename, 'r') do |file| 56 | yield [file, filename] 57 | end 58 | end 59 | end 60 | end 61 | 62 | saw_errors = false 63 | each_input_file do |file, filename| 64 | results = SQLint::Linter.new(filename, file).run.first(options[:limit]) 65 | results.each do |lint| 66 | display_lint(lint) 67 | end 68 | saw_errors ||= results.any? { |lint| lint.type == :error } 69 | end 70 | 71 | exit 1 if saw_errors 72 | -------------------------------------------------------------------------------- /lib/sqlint/linter.rb: -------------------------------------------------------------------------------- 1 | require 'pg_query' 2 | 3 | module SQLint 4 | class Linter 5 | Lint = Struct.new(:filename, :line, :column, :type, :message) 6 | ParseState = Struct.new(:input, :offset) 7 | END_PARSE = ParseState.new(nil, nil) 8 | 9 | def initialize(filename, input_stream) 10 | @input = input_stream.read 11 | @filename = filename 12 | end 13 | 14 | def run 15 | Enumerator.new do |results| 16 | state = ParseState.new(@input, 0) 17 | while state != END_PARSE 18 | error, new_parse_state = parse_next_error(state) 19 | results << error if error 20 | state = new_parse_state 21 | end 22 | end 23 | end 24 | 25 | private 26 | 27 | def parse_next_error(parse_state) 28 | begin 29 | PgQuery.parse(parse_state.input) 30 | [nil, END_PARSE] 31 | rescue PgQuery::ParseError => e 32 | offset = e.location + parse_state.offset 33 | line_number, column_number = find_absolute_position(offset) 34 | lint = Lint.new(@filename, line_number, column_number, :error, clean_message(e.message)) 35 | 36 | input_from_error = parse_state.input[e.location..-1] 37 | semicolon_pos = input_from_error.index(";") if input_from_error 38 | [ 39 | lint, 40 | if semicolon_pos 41 | remaining_input = input_from_error[semicolon_pos+1..-1] 42 | new_offset = offset + semicolon_pos + 1 43 | ParseState.new(remaining_input, new_offset) 44 | else 45 | END_PARSE 46 | end 47 | ] 48 | end 49 | end 50 | 51 | def find_absolute_position(offset) 52 | lines_before_error = @input[0...(offset)].split("\n") 53 | line_number = lines_before_error.size 54 | column_number = lines_before_error.any? ? lines_before_error.last.size : 1 55 | [line_number, column_number] 56 | end 57 | 58 | def clean_message(message) 59 | message 60 | .gsub(/(?<=at or near ")(.*)(?=")/) { |match| match[0..49] } 61 | .gsub(/\s+\(scan\.l\:\d+\)/, '') 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/purcell/sqlint/actions/workflows/ci.yml/badge.svg)](https://github.com/purcell/sqlint/actions/workflows/ci.yml) 2 | [ ![](https://img.shields.io/gem/v/sqlint.svg)](https://rubygems.org/gems/sqlint) 3 | [ ![](https://img.shields.io/gem/dt/sqlint.svg)](https://rubygems.org/gems/sqlint) 4 | Support me 5 | 6 | ## SQLint - a simple SQL linter 7 | 8 | ### About 9 | 10 | SQLint is a simple command-line linter which reads your SQL files and 11 | reports any syntax errors or warnings it finds. 12 | 13 | At this stage, SQLint checks SQL against the ANSI syntax, and uses the 14 | PostgreSQL SQL parser to achieve this. SQLint does not have support 15 | for non-standard SQL variants (e.g. MySQL), but contributions are welcome. 16 | 17 | ### Installation 18 | 19 | SQLint is currently provided as a ruby gem: you can install it using the following command: 20 | 21 | ``` 22 | gem install sqlint 23 | ``` 24 | 25 | ### Usage 26 | 27 | To check the syntax of a file containing SQL, simply pass the filename to `sqlint` on the command line: 28 | 29 | ``` 30 | sqlint filename.sql 31 | ``` 32 | 33 | In the absence of a filename, `sqlint` reads from standard input. 34 | 35 | ### Editor plugins 36 | 37 | Support for `sqlint` is provided for the following editors: 38 | 39 | - Emacs, via [Flycheck](https://github.com/flycheck/flycheck) 40 | - VIM, via [Syntastic](https://github.com/scrooloose/syntastic), [Neomake](https://github.com/neomake/neomake) or [ALE](https://github.com/w0rp/ale) 41 | - SublimeText, via [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter3/) 42 | 43 | 44 | ### Using with pre-commit 45 | 46 | Add this to your `.pre-commit-hooks.yaml`: 47 | 48 | ```yaml 49 | - repo: https://github.com/purcell/sqlint 50 | rev: master 51 | hooks: 52 | - id: sqlint 53 | ``` 54 | 55 | ### Authors 56 | 57 | This software was written by 58 | [Steve Purcell](https://github.com/purcell) and 59 | [Kieran Trezona-le Comte](https://github.com/trezona-lecomte). 60 | 61 | ### License and copyright 62 | 63 | Copyright 2015-2018 Powershop NZ Ltd. 64 | Copyright 2018-2021 Steve Purcell. 65 | MIT license. 66 | 67 |
68 | 69 | [💝 Support this project and my other Open Source work via Patreon](https://www.patreon.com/sanityinc) 70 | 71 | [💼 LinkedIn profile](https://uk.linkedin.com/in/stevepurcell) 72 | 73 | [✍ sanityinc.com](http://www.sanityinc.com/) 74 | -------------------------------------------------------------------------------- /spec/linter_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/sqlint' 2 | 3 | RSpec.describe SQLint::Linter do 4 | WIBBLE_ERROR = 'syntax error at or near "WIBBLE"' 5 | 6 | let(:filename) { "some/file/here.sql" } 7 | let(:input) { "SELECT 1" } 8 | let(:input_stream) { StringIO.new(input) } 9 | subject(:linter) { SQLint::Linter.new(filename, input_stream) } 10 | let(:results) { linter.run.to_a } 11 | 12 | def error(line, col, msg) 13 | SQLint::Linter::Lint.new(filename, line, col, :error, msg) 14 | end 15 | 16 | def warning(line, col, msg) 17 | SQLint::Linter::Lint.new(filename, line, col, :warning, msg) 18 | end 19 | 20 | context "with empty input" do 21 | let(:input) { "" } 22 | 23 | it "reports no errors" do 24 | expect(results).to be_empty 25 | end 26 | end 27 | 28 | context "with valid PG 12 syntax" do 29 | let(:input) do <<-EOF 30 | CREATE TABLE things ( 31 | x_in numeric, 32 | x_cm numeric GENERATED ALWAYS AS (x / 2.54) STORED 33 | ); 34 | EOF 35 | end 36 | 37 | it "reports no errors" do 38 | expect(results).to be_empty 39 | end 40 | end 41 | 42 | describe "single errors" do 43 | context "with a single valid statement" do 44 | it "reports no errors" do 45 | expect(results).to be_empty 46 | end 47 | end 48 | 49 | context "with a single invalid keyword" do 50 | let(:input) { "WIBBLE" } 51 | it "reports one error" do 52 | expect(results).to eq([error(1, 1, WIBBLE_ERROR)]) 53 | end 54 | end 55 | 56 | context "with two successive invalid keywords" do 57 | let(:input) { "WIBBLE WIBBLE" } 58 | it "report 2 errors" do 59 | expect(results).to eq([error(1, 1, WIBBLE_ERROR)]) 60 | end 61 | end 62 | 63 | context "with a single invalid keyword on a later line" do 64 | let(:input) { "SELECT 1;\nWIBBLE" } 65 | it "reports one error" do 66 | expect(results).to eq([error(2, 1, WIBBLE_ERROR)]) 67 | end 68 | end 69 | 70 | context "with a single error part-way through a line" do 71 | let(:input) { "SELECT '" } 72 | it "reports one error" do 73 | expect(results).to eq([error(1, 8, 'unterminated quoted string at or near "\'"')]) 74 | end 75 | end 76 | 77 | context "when there are 2 errors in separate statements" do 78 | let(:input) { "WIBBLE; WIBBLE" } 79 | it "report 2 errors" do 80 | expect(results).to eq([error(1, 1, WIBBLE_ERROR), error(1, 9, WIBBLE_ERROR)]) 81 | end 82 | end 83 | 84 | context "when there is a second error at the end of the file" do 85 | let(:input) { "WIBBLE; SELECT 1 FROM" } 86 | it "reports 2 errors" do 87 | expect(results).to eq([error(1, 1, WIBBLE_ERROR), 88 | error(1, 21, "syntax error at end of input")]) 89 | end 90 | end 91 | end 92 | 93 | describe "long error messages" do 94 | context "when the 'at or near' fragment is longer than 50 characters" do 95 | let(:input) { "SELECT '" + 'x' * 100 } 96 | it "truncates the fragment" do 97 | expect(results).to eq([error(1, 8, "unterminated quoted string at or near \"'#{'x' * 49}\"")]) 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /gemset.nix: -------------------------------------------------------------------------------- 1 | { 2 | bigdecimal = { 3 | groups = ["default"]; 4 | platforms = []; 5 | source = { 6 | remotes = ["https://rubygems.org"]; 7 | sha256 = "1k6qzammv9r6b2cw3siasaik18i6wjc5m0gw5nfdc6jj64h79z1g"; 8 | type = "gem"; 9 | }; 10 | version = "3.1.9"; 11 | }; 12 | diff-lcs = { 13 | groups = ["default" "development"]; 14 | platforms = []; 15 | source = { 16 | remotes = ["https://rubygems.org"]; 17 | sha256 = "1m3cv0ynmxq93axp6kiby9wihpsdj42y6s3j8bsf5a1p7qzsi98j"; 18 | type = "gem"; 19 | }; 20 | version = "1.6.1"; 21 | }; 22 | google-protobuf = { 23 | dependencies = ["bigdecimal" "rake"]; 24 | groups = ["default"]; 25 | platforms = []; 26 | source = { 27 | remotes = ["https://rubygems.org"]; 28 | sha256 = "1v8nmvs1bh82d8j0aidf4slbj5zcrwlc3xmc53ci7kzcps6icd8g"; 29 | type = "gem"; 30 | }; 31 | version = "4.30.2"; 32 | }; 33 | pg_query = { 34 | dependencies = ["google-protobuf"]; 35 | groups = ["default"]; 36 | platforms = []; 37 | source = { 38 | remotes = ["https://rubygems.org"]; 39 | sha256 = "07j86a2mf90dhjlm6ns7p59ij91axg860k63hxc2rw89w8lm404b"; 40 | type = "gem"; 41 | }; 42 | version = "6.1.0"; 43 | }; 44 | rake = { 45 | groups = ["development"]; 46 | platforms = []; 47 | source = { 48 | remotes = ["https://rubygems.org"]; 49 | sha256 = "17850wcwkgi30p7yqh60960ypn7yibacjjha0av78zaxwvd3ijs6"; 50 | type = "gem"; 51 | }; 52 | version = "13.2.1"; 53 | }; 54 | rspec = { 55 | dependencies = ["rspec-core" "rspec-expectations" "rspec-mocks"]; 56 | groups = ["development"]; 57 | platforms = []; 58 | source = { 59 | remotes = ["https://rubygems.org"]; 60 | sha256 = "14xrp8vq6i9zx37vh0yp4h9m0anx9paw200l1r5ad9fmq559346l"; 61 | type = "gem"; 62 | }; 63 | version = "3.13.0"; 64 | }; 65 | rspec-core = { 66 | dependencies = ["rspec-support"]; 67 | groups = ["default" "development"]; 68 | platforms = []; 69 | source = { 70 | remotes = ["https://rubygems.org"]; 71 | sha256 = "1r6zbis0hhbik1ck8kh58qb37d1qwij1x1d2fy4jxkzryh3na4r5"; 72 | type = "gem"; 73 | }; 74 | version = "3.13.3"; 75 | }; 76 | rspec-expectations = { 77 | dependencies = ["diff-lcs" "rspec-support"]; 78 | groups = ["default" "development"]; 79 | platforms = []; 80 | source = { 81 | remotes = ["https://rubygems.org"]; 82 | sha256 = "0n3cyrhsa75x5wwvskrrqk56jbjgdi2q1zx0irllf0chkgsmlsqf"; 83 | type = "gem"; 84 | }; 85 | version = "3.13.3"; 86 | }; 87 | rspec-mocks = { 88 | dependencies = ["diff-lcs" "rspec-support"]; 89 | groups = ["default" "development"]; 90 | platforms = []; 91 | source = { 92 | remotes = ["https://rubygems.org"]; 93 | sha256 = "1vxxkb2sf2b36d8ca2nq84kjf85fz4x7wqcvb8r6a5hfxxfk69r3"; 94 | type = "gem"; 95 | }; 96 | version = "3.13.2"; 97 | }; 98 | rspec-support = { 99 | groups = ["default" "development"]; 100 | platforms = []; 101 | source = { 102 | remotes = ["https://rubygems.org"]; 103 | sha256 = "1v6v6xvxcpkrrsrv7v1xgf7sl0d71vcfz1cnrjflpf6r7x3a58yf"; 104 | type = "gem"; 105 | }; 106 | version = "3.13.2"; 107 | }; 108 | } 109 | --------------------------------------------------------------------------------