Building a JSON validator with Sylver - Part3/3 : From queries to analyzer

Search for a command to run...

No comments yet. Be the first to comment.
Sylver is a language agnostic platform for building custom source code analyzers (think eslint for every language). This might be a lot to unpack, so let us explore this tool by solving a real-world problem: our application's configuration is stored ...
Up to this point, we've been using simple test databases where the data for each row fits within a single page. However, in the wild, it is quite common for a row to be larger than a single page (typically 4096 bytes), especially when using variable-...

The frontend part of our compiler is complete, and we can parse the source code of a pylite program into an AST. This leaves us with a final task: translating the program described by the AST into assembly code. Technically, we could generate assembl...

It has become common practice to start with a "Hello World" program when learning a new programming language. This is a simple program that outputs the text "Hello, World!" to the screen. While writing such a program is a trivial task with most prog...

Compilers are frustrating. Two decades ago, during a boring afternoon in my teenage room, I set out to discover how the software and games I spent so many hours with were made. I plunged into a Google Search rabbit hole that would yield a puzzling a...

In the previous posts, we've explored the SQLite file format and built a simple SQL parser. It's time to put these pieces together and implement a query evaluator! In this post, we'll lay the groundwork for evaluating SQL queries and build a query ev...

In Part1 and Part2 of the series, we learned how to build a language spec and how to use Sylver's query language to explore the parse tree of our JSON documents.
While it can be insightful to explore a codebase interactively through source-code queries, it's not the most practical way to perform source code verification. In this tutorial, we'll learn how to package the queries we built in the last part into a ruleset to use them in a linter-like fashion.
If you have already installed sylver and sylver --version doesn't output
a version number >= 0.1.4, please go to https://sylver.dev to download a fresh
copy of the software.
We'll reuse two files from the last tutorial:
node JsonNode { }
node Null: JsonNode { }
node Bool: JsonNode { }
node Number: JsonNode { }
node String: JsonNode { }
node Array: JsonNode {
elems: List<JsonNode>
}
node Object: JsonNode {
members: List<Member>
}
node Member: JsonNode {
key: String,
value: JsonNode
}
term COMMA = ','
term COLON = ':'
term L_BRACE = '{'
term R_BRACE = '}'
term L_BRACKET = '['
term R_BRACKET = ']'
term NULL = 'null'
term BOOL_LIT = `true|false`
term NUMBER_LIT = `\-?(0|([1-9][0-9]*))(.[0-9]+)?((e|E)(\+|-)?[0-9]+)?`
term STRING_LIT = `"([^"\\]|(\\[\\/bnfrt"])|(\\u[a-fA-F0-9]{4}))*"`
ignore term WHITESPACE = `\s`
rule string = String { STRING_LIT }
rule member = Member { key@string COLON value@main }
rule main =
Null { NULL }
| Number { NUMBER_LIT }
| Bool { BOOL_LIT }
| string
| Array { L_BRACKET elems@sepBy(COMMA, main) R_BRACKET }
| Object { L_BRACE members@sepBy(COMMA, member) R_BRACE }@
{
"variables": [
{
"name": "date of birt`",
"description": "Customer's date of birth",
"type": "datetime"
},
{
"name": "activity",
"description": "A short text describing the customer's profession",
"type": "string"
},
{
"name": "country",
"description": "Customer's country of residence",
"type": "string",
"values": ["us", "fr", "it" ]
}
]
}
Packaging the rules from the previous tutorial into a reusable ruleset is as simple as creating the following YAML file:
id: 'JSON ruleset'
language: json.syl
rules:
- id: variable_length
message: Variable name is too long
category: style
query: >
match String desc when desc.text.length > 37 && desc.parent is {
Member m when m.key.text == '"description"'
}
- id: variable_format
message: Variable name isn't a lowercase word
category: style
query: >
match String s when !s.text.matches(`"[a-z]+"`) && s.parent is {
Member m when m.key.text == '"name"'
}
- id: types_or_values
message: Fields 'type' and 'values' are mutually exclusive
category: error
note: The type can be deduced from the values list.
query: >
match Object n when
any n.members.children match {
Member m when m.key.text == '"type"'
}
&& any n.members.children match {
Member m when m.key.text == '"values"'
}
Where id is a human-readable description of the ruleset, and language refers to
a language spec file.
The following properties describe the individual rules composing the ruleset:
Assuming that our ruleset file is called ruleset.yaml, we can run this ruleset on every .json file in the current directory by invoking the following command:
sylver ruleset run --files "*.json" --rulesets ruleset.yaml
If we wish to validate our codebase against multiple rulesets, repeating the above
command for every ruleset can be tedious. Instead, we can write a project configuration
in a sylver.yaml file at the root of our project:
subprojects:
- language: json.syl
rulesets: ['ruleset.yaml']
include:
- './**/*.json'
The configuration contains a list of subprojects, each having a language, an optional list of rulesets, and a list of files to include.
Invoking sylver check will read the config from sylver.yaml and run the
specified rulesets.
Should you want to reuse your language specs or rulesets in several projects, copying your .syl and .yaml files in every project would be inconvenient. Luckily
rulesets and project configurations can refer to artifacts stored in a git repository.
The language spec and ruleset for this tutorial have been uploaded to this repo, so if we rewrite our sylver.yaml config file as:
subprojects:
- language:
repo: https://github.com/geoffreycopin/getting_started_json_tutorial
file: json.syl
rulesets:
- repo: https://github.com/geoffreycopin/getting_started_json_tutorial
file: 'ruleset.yaml'
include:
- './**/*.json'
the language spec and ruleset will be cloned automatically in the .sylver directory
when running sylver check.
We now have a reusable linter for our JSON configuration files built from scratch using Sylver's DSL.
The following tutorial will use a pre-built Golang to write a general-purpose Go linter.