hcl-checker 1.6.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +27 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/Gemfile +0 -9
- data/Gemfile.lock +27 -43
- data/README.md +124 -20
- data/assets/lexer.rex +120 -20
- data/assets/parse.y +174 -26
- data/bin/setup +6 -0
- data/hcl-checker.gemspec +5 -14
- data/lib/hcl/checker/lexer.rb +119 -21
- data/lib/hcl/checker/parser.rb +785 -155
- data/lib/hcl/checker/version.rb +1 -1
- data/lib/hcl/checker.rb +3 -6
- metadata +15 -38
- data/.travis.yml +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 377ff190a52bcc216535c048cab19155c0f07c22a246740678f58171d4b17d6e
|
|
4
|
+
data.tar.gz: a9b1b18a57d077470ebfd8c27629a7dbd5769d2eafb2525ce809613f78f0b351
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5dcdcff0e7169f375e045a9e27eb5fd7f0f98046b57f021884eff0dd70b8afaf0be22c6d34d0f460374efcfce766f91cfa4aec7fe85baa7d7c345bf297d0a6b7
|
|
7
|
+
data.tar.gz: 63cefafcb6395a016c5403f8ea9a639d3a6f25311a2de5349547aa15a2a010337f730ed9d164c8eb57331cd33231552484fbbba422594c5c7bd723fb44279bec
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
ruby: ['3.3', '3.4', '4.0']
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Ruby
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: ${{ matrix.ruby }}
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: bundle exec rake spec
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.4.10
|
data/Gemfile
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
|
-
# This is needed due https://nvd.nist.gov/vuln/detail/CVE-2018-14404
|
|
4
|
-
# A NULL pointer dereference vulnerability exists in the xpath.c:xmlXPathCompOpEval()
|
|
5
|
-
# function of libxml2 through 2.9.8 when parsing an invalid XPath expression in the
|
|
6
|
-
# XPATH_OP_AND or XPATH_OP_OR case. Applications processing untrusted XSL format inputs
|
|
7
|
-
# with the use of the libxml2 library may be vulnerable to a denial of service attack due
|
|
8
|
-
# to a crash of the application.
|
|
9
|
-
# Nokogiri >= 1.8.5 solves this problem
|
|
10
|
-
gem 'nokogiri', '>= 1.14.2'
|
|
11
|
-
|
|
12
3
|
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
|
13
4
|
|
|
14
5
|
# Specify your gem's dependencies in hcl-checker.gemspec
|
data/Gemfile.lock
CHANGED
|
@@ -1,60 +1,44 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
hcl-checker (
|
|
4
|
+
hcl-checker (2.0.0)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
8
8
|
specs:
|
|
9
|
-
diff-lcs (1.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
rex (2.0.12)
|
|
23
|
-
filesize (~> 0)
|
|
24
|
-
jsobfu (~> 0)
|
|
25
|
-
json (~> 2)
|
|
26
|
-
metasm (~> 1)
|
|
27
|
-
nokogiri (~> 1)
|
|
28
|
-
rb-readline (~> 0)
|
|
29
|
-
rexical (1.0.7)
|
|
30
|
-
rkelly-remix (0.0.7)
|
|
31
|
-
rspec (3.11.0)
|
|
32
|
-
rspec-core (~> 3.11.0)
|
|
33
|
-
rspec-expectations (~> 3.11.0)
|
|
34
|
-
rspec-mocks (~> 3.11.0)
|
|
35
|
-
rspec-core (3.11.0)
|
|
36
|
-
rspec-support (~> 3.11.0)
|
|
37
|
-
rspec-expectations (3.11.0)
|
|
9
|
+
diff-lcs (1.6.2)
|
|
10
|
+
getoptlong (0.2.1)
|
|
11
|
+
racc (1.8.1)
|
|
12
|
+
rake (13.4.2)
|
|
13
|
+
rexical (1.0.8)
|
|
14
|
+
getoptlong
|
|
15
|
+
rspec (3.13.2)
|
|
16
|
+
rspec-core (~> 3.13.0)
|
|
17
|
+
rspec-expectations (~> 3.13.0)
|
|
18
|
+
rspec-mocks (~> 3.13.0)
|
|
19
|
+
rspec-core (3.13.6)
|
|
20
|
+
rspec-support (~> 3.13.0)
|
|
21
|
+
rspec-expectations (3.13.5)
|
|
38
22
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
39
|
-
rspec-support (~> 3.
|
|
40
|
-
rspec-mocks (3.
|
|
23
|
+
rspec-support (~> 3.13.0)
|
|
24
|
+
rspec-mocks (3.13.8)
|
|
41
25
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
42
|
-
rspec-support (~> 3.
|
|
43
|
-
rspec-support (3.
|
|
26
|
+
rspec-support (~> 3.13.0)
|
|
27
|
+
rspec-support (3.13.7)
|
|
44
28
|
|
|
45
29
|
PLATFORMS
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
aarch64-linux-gnu
|
|
31
|
+
arm64-darwin
|
|
32
|
+
x86_64-darwin
|
|
33
|
+
x86_64-linux-gnu
|
|
48
34
|
|
|
49
35
|
DEPENDENCIES
|
|
50
|
-
bundler (~> 2.4
|
|
36
|
+
bundler (~> 2.4)
|
|
51
37
|
hcl-checker!
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
rex (= 2.0.12)
|
|
56
|
-
rexical (>= 1.0.7)
|
|
38
|
+
racc (~> 1.8)
|
|
39
|
+
rake (~> 13.0)
|
|
40
|
+
rexical (>= 1.0.8)
|
|
57
41
|
rspec (~> 3.0)
|
|
58
42
|
|
|
59
43
|
BUNDLED WITH
|
|
60
|
-
2.4.
|
|
44
|
+
2.4.22
|
data/README.md
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
**Hashicorp Configuration Language** syntax checker and parser.
|
|
4
4
|
|
|
5
|
-
Parser originally created by [Sikula](https://github.com/sikula) and available
|
|
6
|
-
at [Ruby HCL Repository](https://github.com/sikula/ruby-hcl). Only works with
|
|
7
|
-
[HCL Version 1](https://github.com/hashicorp/hcl).
|
|
8
|
-
|
|
9
5
|
## Installation
|
|
10
6
|
|
|
11
7
|
Add this line to your application's Gemfile:
|
|
@@ -26,7 +22,7 @@ Or install it yourself as:
|
|
|
26
22
|
|
|
27
23
|
Load HCL string:
|
|
28
24
|
|
|
29
|
-
```
|
|
25
|
+
```ruby
|
|
30
26
|
hcl_string = 'provider "aws" {
|
|
31
27
|
region = "${var.aws_region}"
|
|
32
28
|
access_key = "${var.aws_access_key}"
|
|
@@ -46,45 +42,153 @@ resource "aws_vpc" "default" {
|
|
|
46
42
|
You can validate the `hcl_string` contents with `valid?` method. This will
|
|
47
43
|
return `true` if is a valid HCL or `false` if not.
|
|
48
44
|
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
=> true
|
|
45
|
+
```ruby
|
|
46
|
+
HCL::Checker.valid? hcl_string
|
|
47
|
+
# => true
|
|
52
48
|
```
|
|
53
49
|
|
|
54
50
|
You can parse the `hcl_string` into a `Hash` with `parse` method.
|
|
55
51
|
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
=> {"provider"=>{"aws"=>{"region"=>"${var.aws_region}", "access_key"=>"${var.aws_access_key}", "secret_key"=>"${var.aws_secret_key}"}}, "resource"=>{"aws_vpc"=>{"default"=>{"cidr_block"=>"10.0.0.0/16", "enable_dns_hostnames"=>true, "tags"=>{"Name"=>"Event Store VPC"}}}}}
|
|
52
|
+
```ruby
|
|
53
|
+
HCL::Checker.parse(hcl_string)
|
|
54
|
+
# => {"provider"=>{"aws"=>{"region"=>"${var.aws_region}", "access_key"=>"${var.aws_access_key}", "secret_key"=>"${var.aws_secret_key}"}}, "resource"=>{"aws_vpc"=>{"default"=>{"cidr_block"=>"10.0.0.0/16", "enable_dns_hostnames"=>true, "tags"=>{"Name"=>"Event Store VPC"}}}}}
|
|
59
55
|
```
|
|
60
56
|
|
|
61
|
-
If after a `parse` you got `
|
|
57
|
+
If after a `parse` you got a `String` back instead of a `Hash`, that string
|
|
58
|
+
is the parse error message, and it is also available afterwards via
|
|
59
|
+
`last_error`:
|
|
62
60
|
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
=> "Parse error
|
|
61
|
+
```ruby
|
|
62
|
+
HCL::Checker.last_error
|
|
63
|
+
# => "Parse error on line 7, column 18: unexpected \",\" (after \"instance_type\")"
|
|
66
64
|
```
|
|
67
65
|
|
|
66
|
+
## Compatibility
|
|
67
|
+
|
|
68
|
+
This gem parses full [HCL1](https://github.com/hashicorp/hcl) syntax, plus a
|
|
69
|
+
substantial subset of HCL2 expression syntax: arithmetic/comparison/logical
|
|
70
|
+
operators, ternary expressions, `for` expressions (list and object
|
|
71
|
+
comprehensions), splat expressions (`list[*]` and the legacy `list.*`),
|
|
72
|
+
chained indexing/attribute access, the `null` literal, scientific notation,
|
|
73
|
+
string escape sequences, `<<-` heredoc dedenting, and `%{ if }`/`%{ for }`
|
|
74
|
+
string template directives.
|
|
75
|
+
|
|
76
|
+
Not supported:
|
|
77
|
+
- The JSON variant of HCL2 (`.tf.json` files) — a fully separate parsing mode.
|
|
78
|
+
- Expression evaluation — this is a structural parser/checker, not an
|
|
79
|
+
interpreter. `${...}` interpolations and bare expressions (function calls,
|
|
80
|
+
arithmetic, `for`/ternary, etc.) are preserved as opaque text, never
|
|
81
|
+
evaluated.
|
|
82
|
+
|
|
83
|
+
Known caveats:
|
|
84
|
+
- A quoted string used as an operand inside a larger expression loses its
|
|
85
|
+
surrounding quotes in the reconstructed text (e.g. `var.env == "prod"`
|
|
86
|
+
becomes the text `var.env == prod`). A string assigned directly as a value
|
|
87
|
+
is unaffected (`region = "us-east-1"` stays exactly `"us-east-1"`).
|
|
88
|
+
- A list/object literal used as an operand inside a larger expression is
|
|
89
|
+
rendered with Ruby's own `Array`/`Hash` text representation, not HCL
|
|
90
|
+
syntax (e.g. `merge({a = 1}, {b = 2})` becomes the text
|
|
91
|
+
`merge({"a"=>1}, {"b"=>2})`).
|
|
92
|
+
|
|
93
|
+
### Upgrading from 1.x
|
|
94
|
+
|
|
95
|
+
Version 2.0 adds the HCL2 support described above and fixes several parsing
|
|
96
|
+
bugs (including two that could hang or silently return wrong output). Most
|
|
97
|
+
HCL1 configs continue to parse identically, but a few things changed:
|
|
98
|
+
|
|
99
|
+
- **Ruby 3.3 or newer is now required** (previously `>= 2.7.0`).
|
|
100
|
+
- **`null` now parses as a real `nil`**, not the string `"null"`.
|
|
101
|
+
- **Parse error messages changed format** and now include a line and column
|
|
102
|
+
number, e.g. `"Parse error on line 7, column 18: unexpected \",\" (after
|
|
103
|
+
\"instance_type\")"`. Code that pattern-matches the exact old error text
|
|
104
|
+
will need updating; code that only checks whether `parse` returned a
|
|
105
|
+
`Hash` or a `String` (the documented, recommended pattern above) is
|
|
106
|
+
unaffected.
|
|
107
|
+
- `for`, `in`, `if` and `null` are now reserved words when they appear as a
|
|
108
|
+
standalone token, needed for HCL2's `for` expressions and the `null`
|
|
109
|
+
literal. They still work as plain, unquoted attribute names and block
|
|
110
|
+
labels (`for = 1`, `resource for "x" {}`), but not in every conceivable
|
|
111
|
+
position — a nested block literally *labeled* `for` immediately followed
|
|
112
|
+
by another bare identifier no longer parses, an extremely unusual
|
|
113
|
+
construct in practice.
|
|
114
|
+
|
|
68
115
|
## Development
|
|
69
116
|
|
|
70
117
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
71
118
|
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
|
72
119
|
prompt that will allow you to experiment.
|
|
73
120
|
|
|
74
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
121
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
122
|
+
|
|
123
|
+
> **Note:** if you run `bundle install` directly instead of `bin/setup` and
|
|
124
|
+
> get a `Bundler::PermissionError` trying to write to something like
|
|
125
|
+
> `/var/lib/gems/3.3.0/cache`, your system Ruby's gem directory is
|
|
126
|
+
> root-owned (common when Ruby was installed via a distro package, e.g.
|
|
127
|
+
> apt's `ruby3.3`). Fix it by installing into a local, writable path
|
|
128
|
+
> instead: `bundle config set --local path 'vendor/bundle' && bundle
|
|
129
|
+
> install` — this is exactly what `bin/setup` already does for you.
|
|
79
130
|
|
|
80
131
|
### Building grammar
|
|
81
132
|
|
|
133
|
+
If you change `assets/lexer.rex` or `assets/parse.y`, regenerate the lexer
|
|
134
|
+
and parser with:
|
|
135
|
+
|
|
82
136
|
```
|
|
83
137
|
$ bundle exec rake build_grammar
|
|
84
138
|
Building Lexer....done
|
|
85
139
|
Building Parser....done
|
|
86
140
|
```
|
|
87
141
|
|
|
142
|
+
This overwrites `lib/hcl/checker/lexer.rb` and `lib/hcl/checker/parser.rb` —
|
|
143
|
+
never edit those two files directly, they're marked `DO NOT MODIFY` and any
|
|
144
|
+
changes will be lost the next time this task runs.
|
|
145
|
+
|
|
146
|
+
## Publishing to RubyGems.org
|
|
147
|
+
|
|
148
|
+
This project uses Bundler's standard gem release workflow (`rake release`,
|
|
149
|
+
provided by `bundler/gem_tasks` in the `Rakefile`).
|
|
150
|
+
|
|
151
|
+
1. **Bump the version** in `lib/hcl/checker/version.rb`.
|
|
152
|
+
2. **Run the test suite** and make sure it's green: `bundle exec rake spec`.
|
|
153
|
+
3. **Commit the version bump**, e.g. `git commit -am "Bump version to X.Y.Z"`.
|
|
154
|
+
4. **Release**:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
$ bundle exec rake release
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This single command builds the `.gem` file from `hcl-checker.gemspec`,
|
|
161
|
+
creates a git tag `vX.Y.Z`, pushes the tag and any pending commits to the
|
|
162
|
+
configured remote, and pushes the built gem to
|
|
163
|
+
[rubygems.org](https://rubygems.org).
|
|
164
|
+
|
|
165
|
+
If you've never pushed to rubygems.org from this machine before, `gem push`
|
|
166
|
+
will prompt you to sign in (or run `gem signin` beforehand to store an API
|
|
167
|
+
key). This gem has enough downloads that RubyGems requires multi-factor
|
|
168
|
+
authentication to push a new version — have your TOTP code ready, or pass it
|
|
169
|
+
directly with `gem push --otp <code> pkg/hcl-checker-X.Y.Z.gem` if you need
|
|
170
|
+
to push the built `.gem` file manually instead of via `rake release`.
|
|
171
|
+
|
|
172
|
+
To build the `.gem` file without publishing it — useful to double check its
|
|
173
|
+
contents first — run `bundle exec rake build`; the file is written to `pkg/`.
|
|
174
|
+
|
|
175
|
+
## Roadmap / Future work
|
|
176
|
+
|
|
177
|
+
- [ ] `HCL::Checker.last_error` is mutable class-level state (not thread-safe).
|
|
178
|
+
- [ ] `parse` returns a `Hash` on success and a `String` (error message) on
|
|
179
|
+
failure — an ambiguous return type that forces callers to duck-type
|
|
180
|
+
the result instead of, say, raising a dedicated exception class.
|
|
181
|
+
- [ ] No JSON variant of HCL2 (`.tf.json`) support.
|
|
182
|
+
- [ ] No expression evaluation — by design (see Compatibility above), but
|
|
183
|
+
worth revisiting if there's ever demand for it as a separate,
|
|
184
|
+
opt-in mode.
|
|
185
|
+
|
|
186
|
+
## Implementation details
|
|
187
|
+
|
|
188
|
+
Initial parser version with partial HCL 1.0 suppport was created and developed
|
|
189
|
+
by [Sikula](https://github.com/sikula)
|
|
190
|
+
and available at [Ruby HCL Repository](https://github.com/sikula/ruby-hcl).
|
|
191
|
+
|
|
88
192
|
## Contributing
|
|
89
193
|
|
|
90
194
|
Bug reports and pull requests are welcome on GitHub at
|
data/assets/lexer.rex
CHANGED
|
@@ -8,13 +8,34 @@ macro
|
|
|
8
8
|
COMMENT \#.*|\/\/.*$
|
|
9
9
|
MCOMMENTIN \/\*
|
|
10
10
|
BOOL true|false
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
NULL null(?![a-zA-Z0-9_\-])
|
|
12
|
+
FOR for(?![a-zA-Z0-9_\-])
|
|
13
|
+
IN in(?![a-zA-Z0-9_\-])
|
|
14
|
+
IF if(?![a-zA-Z0-9_\-])
|
|
15
|
+
NUMBER \d+
|
|
16
|
+
FLOAT \d+(\.\d+)?[eE][-+]?\d+|\d+\.\d+
|
|
13
17
|
COMMA \,
|
|
14
|
-
IDENTIFIER [a-zA-Z_][a-zA-Z0-9_
|
|
18
|
+
IDENTIFIER [a-zA-Z_][a-zA-Z0-9_\-]*
|
|
15
19
|
EQUAL \=
|
|
16
20
|
QUOTE \"
|
|
17
21
|
MINUS \-
|
|
22
|
+
PLUS \+
|
|
23
|
+
STAR \*
|
|
24
|
+
SLASH \/
|
|
25
|
+
PERCENT \%
|
|
26
|
+
ANDAND \&\&
|
|
27
|
+
OROR \|\|
|
|
28
|
+
EQEQ \=\=
|
|
29
|
+
NEQ \!\=
|
|
30
|
+
LE \<\=
|
|
31
|
+
GE \>\=
|
|
32
|
+
LT \<
|
|
33
|
+
GT \>
|
|
34
|
+
ARROW \=\>
|
|
35
|
+
QUESTION \?
|
|
36
|
+
COLON \:
|
|
37
|
+
PERIOD \.
|
|
38
|
+
BANG \!
|
|
18
39
|
LEFTBRACE \{
|
|
19
40
|
RIGHTBRACE \}
|
|
20
41
|
LEFTBRACKET \[
|
|
@@ -32,10 +53,14 @@ rule
|
|
|
32
53
|
#-------------------------------------------------------------------------------
|
|
33
54
|
{MCOMMENTIN} { consume_comment(text) }
|
|
34
55
|
{BOOL} { [:BOOL, to_boolean(text)]}
|
|
56
|
+
{NULL} { [:NULL, text]}
|
|
57
|
+
{FOR} { [:FOR, text]}
|
|
58
|
+
{IN} { [:IN, text]}
|
|
59
|
+
{IF} { [:IF, text]}
|
|
35
60
|
{FLOAT} { [:FLOAT, text.to_f] }
|
|
36
61
|
{NUMBER} { [:NUMBER, text.to_i] }
|
|
37
62
|
{QUOTE} { [:STRING, consume_string(text)] }
|
|
38
|
-
{HEREDOCUMENT} { [:STRING, consume_heredoc] }
|
|
63
|
+
{HEREDOCUMENT} { [:STRING, consume_heredoc(text.end_with?('-'))] }
|
|
39
64
|
#-------------------------------------------------------------------------------
|
|
40
65
|
{LEFTBRACE} { [:LEFTBRACE, text]}
|
|
41
66
|
{RIGHTBRACE} { [:RIGHTBRACE, text]}
|
|
@@ -46,30 +71,62 @@ rule
|
|
|
46
71
|
#-------------------------------------------------------------------------------
|
|
47
72
|
{COMMA} { [:COMMA, text]}
|
|
48
73
|
{IDENTIFIER} { [:IDENTIFIER, text]}
|
|
74
|
+
#-- two-character operators must come before their one-character prefixes,
|
|
75
|
+
#-- since this scanner uses first-match-wins (not longest-match-wins).
|
|
76
|
+
{EQEQ} { [:EQEQ, text]}
|
|
77
|
+
{NEQ} { [:NEQ, text]}
|
|
78
|
+
{LE} { [:LE, text]}
|
|
79
|
+
{GE} { [:GE, text]}
|
|
80
|
+
{ARROW} { [:ARROW, text]}
|
|
81
|
+
{LT} { [:LT, text]}
|
|
82
|
+
{GT} { [:GT, text]}
|
|
49
83
|
{EQUAL} { [:EQUAL, text]}
|
|
50
84
|
{MINUS} { [:MINUS, text]}
|
|
85
|
+
{PLUS} { [:PLUS, text]}
|
|
86
|
+
{STAR} { [:STAR, text]}
|
|
87
|
+
{SLASH} { [:SLASH, text]}
|
|
88
|
+
{PERCENT} { [:PERCENT, text]}
|
|
89
|
+
{ANDAND} { [:ANDAND, text]}
|
|
90
|
+
{OROR} { [:OROR, text]}
|
|
91
|
+
{QUESTION} { [:QUESTION, text]}
|
|
92
|
+
{COLON} { [:COLON, text]}
|
|
93
|
+
{PERIOD} { [:PERIOD, text]}
|
|
94
|
+
{BANG} { [:BANG, text]}
|
|
51
95
|
|
|
52
96
|
inner
|
|
97
|
+
attr_reader :token_positions
|
|
98
|
+
|
|
53
99
|
def lex(input)
|
|
54
100
|
scan_setup(input)
|
|
55
101
|
tokens = []
|
|
102
|
+
@token_positions = []
|
|
103
|
+
|
|
56
104
|
while token = next_token
|
|
57
105
|
tokens << token
|
|
106
|
+
@token_positions << current_position
|
|
58
107
|
end
|
|
59
108
|
|
|
60
109
|
tokens
|
|
61
110
|
end
|
|
62
111
|
|
|
112
|
+
# Position (line, column) right after the token that was just consumed.
|
|
113
|
+
# Good enough for error reporting: for the overwhelming majority of
|
|
114
|
+
# tokens (identifiers, punctuation, keywords, numbers) which never span
|
|
115
|
+
# multiple lines, this is exactly where the token is. It only becomes an
|
|
116
|
+
# approximation (pointing at the end rather than the start) for
|
|
117
|
+
# constructs that can span several lines, such as heredocs, multi-line
|
|
118
|
+
# strings or block comments.
|
|
119
|
+
def current_position
|
|
120
|
+
consumed = @ss.string[0...@ss.pos]
|
|
121
|
+
line = consumed.count("\n") + 1
|
|
122
|
+
column = consumed.length - (consumed.rindex("\n") || -1)
|
|
123
|
+
|
|
124
|
+
[line, column]
|
|
125
|
+
end
|
|
126
|
+
|
|
63
127
|
|
|
64
128
|
def to_boolean(input)
|
|
65
|
-
|
|
66
|
-
when /true/
|
|
67
|
-
true
|
|
68
|
-
when /false/
|
|
69
|
-
false
|
|
70
|
-
else
|
|
71
|
-
raise "Invalid value for `to_boolean`, expected true/false got #{input}"
|
|
72
|
-
end
|
|
129
|
+
input == 'true'
|
|
73
130
|
end
|
|
74
131
|
|
|
75
132
|
|
|
@@ -79,11 +136,11 @@ inner
|
|
|
79
136
|
until nested.zero?
|
|
80
137
|
case(text = @ss.scan_until(%r{/\*|\*/|\z}) )
|
|
81
138
|
when %r{/\*\z}
|
|
82
|
-
nested
|
|
139
|
+
nested += 1
|
|
83
140
|
when %r{\*/\z}
|
|
84
141
|
nested -= 1
|
|
85
142
|
else
|
|
86
|
-
|
|
143
|
+
raise ScanError, 'unterminated block comment'
|
|
87
144
|
end
|
|
88
145
|
end
|
|
89
146
|
end
|
|
@@ -94,13 +151,18 @@ inner
|
|
|
94
151
|
nested = 0
|
|
95
152
|
|
|
96
153
|
loop do
|
|
97
|
-
|
|
154
|
+
text = @ss.scan_until(%r{\"|\$\{|%\{|\}|\\})
|
|
155
|
+
raise ScanError, 'unterminated string' if text.nil?
|
|
156
|
+
|
|
157
|
+
case text
|
|
98
158
|
when %r{\$\{\z}
|
|
99
159
|
nested += 1
|
|
160
|
+
when %r{%\{\z}
|
|
161
|
+
nested += 1
|
|
100
162
|
when %r{\}\z}
|
|
101
163
|
nested -= 1 if nested.positive?
|
|
102
164
|
when %r{\\\z}
|
|
103
|
-
result += text.chop +
|
|
165
|
+
result += text.chop + decode_escape
|
|
104
166
|
next
|
|
105
167
|
end
|
|
106
168
|
|
|
@@ -112,10 +174,48 @@ inner
|
|
|
112
174
|
result.chop
|
|
113
175
|
end
|
|
114
176
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
|
|
177
|
+
def decode_escape
|
|
178
|
+
char = @ss.getch
|
|
179
|
+
|
|
180
|
+
case char
|
|
181
|
+
when 'n' then "\n"
|
|
182
|
+
when 't' then "\t"
|
|
183
|
+
when 'r' then "\r"
|
|
184
|
+
when '"' then '"'
|
|
185
|
+
when '\\' then '\\'
|
|
186
|
+
when 'u'
|
|
187
|
+
hex = @ss.scan(/[0-9a-fA-F]{4}/)
|
|
188
|
+
hex ? [hex.to_i(16)].pack('U') : 'u'
|
|
189
|
+
when 'U'
|
|
190
|
+
hex = @ss.scan(/[0-9a-fA-F]{8}/)
|
|
191
|
+
hex ? [hex.to_i(16)].pack('U') : 'U'
|
|
192
|
+
else
|
|
193
|
+
char.to_s
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def consume_heredoc(indented)
|
|
198
|
+
marker_line = @ss.scan_until(%r{\n})
|
|
199
|
+
raise ScanError, 'unterminated heredoc: missing marker' if marker_line.nil?
|
|
200
|
+
|
|
201
|
+
marker = marker_line.strip
|
|
202
|
+
closing = /^[ \t]*#{Regexp.escape(marker)}[ \t]*(?:\n|\z)/
|
|
203
|
+
raw = @ss.scan_until(closing)
|
|
204
|
+
raise ScanError, "unterminated heredoc: missing closing marker '#{marker}'" if raw.nil?
|
|
205
|
+
|
|
206
|
+
content = raw.sub(closing, '')
|
|
207
|
+
|
|
208
|
+
indented ? dedent(content) : content
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Strips the common leading whitespace shared by every non-blank line, as
|
|
212
|
+
# required for the `<<-` heredoc form. Blank (or whitespace-only) lines
|
|
213
|
+
# are ignored when computing how much to strip, and are left untouched.
|
|
214
|
+
def dedent(text)
|
|
215
|
+
lines = text.split("\n", -1)
|
|
216
|
+
indents = lines.reject { |line| line.strip.empty? }.map { |line| line[/\A[ \t]*/].length }
|
|
217
|
+
indent = indents.min || 0
|
|
118
218
|
|
|
119
|
-
|
|
219
|
+
lines.map { |line| line.strip.empty? ? line : line[indent..] }.join("\n")
|
|
120
220
|
end
|
|
121
221
|
end
|