diff --git a/docs/lint.markdown b/docs/lint.markdown index 9272e997..1b28cc04 100644 --- a/docs/lint.markdown +++ b/docs/lint.markdown @@ -3,7 +3,7 @@ Linting ```sh jsonschema lint [schemas-or-directories...] [--http/-h] [--fix/-f] - [--json/-j] [--verbose/-v] [--debug/-g] + [--format/-m] [--keep-ordering/-k] [--json/-j] [--verbose/-v] [--debug/-g] [--resolve/-r ...] [--extension/-e ] [--ignore/-i ] [--exclude/-x ] [--only/-o ] [--list/-l] @@ -30,6 +30,12 @@ automatically fix many of them. **The `--fix/-f` option is not supported when passing YAML schemas.** +**The `--format/-m` option requires `--fix/-f` to be set and is not supported +for YAML schemas.** When `--format/-m` is set, the output file is always +written with proper formatting (equivalent to running `fmt`), even if there +are no lint issues to fix. Use `--keep-ordering/-k` with `--format/-m` to +preserve key ordering during formatting. + > [!NOTE] > There are linting rules that require compiling and validating instance > against the given schema. For example, there is a rule to check that the @@ -173,10 +179,16 @@ jsonschema lint path/to/my/schema.json --fix jsonschema lint path/to/my/schema.json --fix --indentation 4 ``` -### Fix lint warnings on a single schema while preserving keyword ordering +### Fix lint warnings and format the schema + +```sh +jsonschema lint path/to/my/schema.json --fix --format +``` + +### Fix lint warnings, format, but preserve keyword ordering ```sh -jsonschema lint path/to/my/schema.json --fix --keep-ordering +jsonschema lint path/to/my/schema.json --fix --format --keep-ordering ``` ### Print a summary of all enabled rules diff --git a/src/command_lint.cc b/src/command_lint.cc index 54d77926..b6bdea99 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -7,7 +7,7 @@ #include #include // EXIT_FAILURE -#include // std::ofstream +#include // std::ofstream, std::ifstream #include // std::cerr, std::cout #include // std::accumulate #include // std::ostringstream @@ -181,6 +181,18 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) return; } + const bool format_output{options.contains("format")}; + const bool keep_ordering{options.contains("keep-ordering")}; + + if (format_output && !options.contains("fix")) { + throw OptionConflictError{"The --format option requires --fix to be set"}; + } + + if (keep_ordering && !format_output) { + throw OptionConflictError{ + "The --keep-ordering option requires --format to be set"}; + } + bool result{true}; auto errors_array = sourcemeta::core::JSON::make_array(); std::vector scores; @@ -255,7 +267,25 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) result = false; } - if (copy != entry.second) { + if (format_output) { + if (!keep_ordering) { + sourcemeta::core::format(copy, sourcemeta::core::schema_walker, + custom_resolver, dialect); + } + + std::ostringstream expected; + sourcemeta::core::prettify(copy, expected, indentation); + expected << "\n"; + + std::ifstream current_stream{entry.first}; + std::ostringstream current; + current << current_stream.rdbuf(); + + if (current.str() != expected.str()) { + std::ofstream output{entry.first}; + output << expected.str(); + } + } else if (copy != entry.second) { std::ofstream output{entry.first}; sourcemeta::core::prettify(copy, output, indentation); output << "\n"; diff --git a/src/main.cc b/src/main.cc index 356be1c3..eded264d 100644 --- a/src/main.cc +++ b/src/main.cc @@ -78,14 +78,17 @@ Global Options: Format the input schemas in-place or check they are formatted. This command does not support YAML schemas yet. - lint [schemas-or-directories...] [--fix/-f] [--extension/-e ] + lint [schemas-or-directories...] [--fix/-f] [--format/-m] + [--keep-ordering/-k] [--extension/-e ] [--ignore/-i ] [--exclude/-x ] [--only/-o ] [--list/-l] [--indentation/-n ] Lint the input schemas and potentially fix the reported issues. The --fix/-f option is not supported when passing YAML schemas. + Use --format/-m with --fix to format the output even when there + are no linting issues. + Use --keep-ordering/-k with --format to preserve key order. Use --list/-l to print a summary of all enabled rules. - Use --indentation/-n to keep indentation when auto-fixing bundle [--extension/-e ] [--ignore/-i ] [--without-id/-w] @@ -146,6 +149,8 @@ auto jsonschema_main(const std::string &program, const std::string &command, return EXIT_SUCCESS; } else if (command == "lint") { app.flag("fix", {"f"}); + app.flag("format", {"m"}); + app.flag("keep-ordering", {"k"}); app.flag("list", {"l"}); app.option("extension", {"e"}); app.option("exclude", {"x"}); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9a7fbf9d..2474c5a0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -533,6 +533,14 @@ add_jsonschema_test_unix(lint/fail_draft7_defs_ref_target) add_jsonschema_test_unix(lint/fail_draft7_defs_ref_target_fix) add_jsonschema_test_unix(lint/fail_draft4_x_keyword_ref_target) add_jsonschema_test_unix(lint/fail_draft4_x_keyword_ref_target_fix) +add_jsonschema_test_unix(lint/pass_lint_fix_format) +add_jsonschema_test_unix(lint/pass_lint_fix_format_no_issues) +add_jsonschema_test_unix(lint/pass_lint_fix_format_keep_ordering) +add_jsonschema_test_unix(lint/pass_lint_fix_format_directory) +add_jsonschema_test_unix(lint/fail_lint_fix_format_unfixable) +add_jsonschema_test_unix(lint/fail_lint_format_without_fix) +add_jsonschema_test_unix(lint/fail_lint_keep_ordering_without_format) +add_jsonschema_test_unix(lint/fail_lint_format_yaml) # Encode add_jsonschema_test_unix(encode/pass_schema_less) diff --git a/test/lint/fail_lint_fix_format_unfixable.sh b/test/lint/fail_lint_fix_format_unfixable.sh new file mode 100755 index 00000000..6636a48b --- /dev/null +++ b/test/lint/fail_lint_fix_format_unfixable.sh @@ -0,0 +1,76 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "title": "Test", + "type": "string", + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "https://example.com" +} +EOF + +cd "$TMP" +"$1" lint "$TMP/schema.json" --fix --format >"$TMP/output.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "2" || exit 1 + +cat << 'EOF' > "$TMP/expected_output.txt" +schema.json:1:1: + Set a non-empty description at the top level of the schema to explain what the definition is about in detail (top_level_description) + at location "" +schema.json:1:1: + Set a non-empty examples array at the top level of the schema to illustrate the expected data (top_level_examples) + at location "" +EOF + +diff "$TMP/output.txt" "$TMP/expected_output.txt" + +cat << 'EOF' > "$TMP/expected.json" +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "https://example.com", + "title": "Test", + "type": "string" +} +EOF + +diff "$TMP/schema.json" "$TMP/expected.json" + +# JSON output +"$1" lint "$TMP/schema.json" --fix --format --json >"$TMP/output_json.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "2" || exit 1 + +cat << EOF > "$TMP/expected_output_json.txt" +{ + "valid": false, + "health": 0, + "errors": [ + { + "path": "$(realpath "$TMP")/schema.json", + "id": "top_level_description", + "message": "Set a non-empty description at the top level of the schema to explain what the definition is about in detail", + "description": null, + "schemaLocation": "", + "position": [ 1, 1, 6, 1 ] + }, + { + "path": "$(realpath "$TMP")/schema.json", + "id": "top_level_examples", + "message": "Set a non-empty examples array at the top level of the schema to illustrate the expected data", + "description": null, + "schemaLocation": "", + "position": [ 1, 1, 6, 1 ] + } + ] +} +EOF + +diff "$TMP/output_json.txt" "$TMP/expected_output_json.txt" diff --git a/test/lint/fail_lint_format_without_fix.sh b/test/lint/fail_lint_format_without_fix.sh new file mode 100755 index 00000000..f9c2a1b5 --- /dev/null +++ b/test/lint/fail_lint_format_without_fix.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "string" +} +EOF + +"$1" lint "$TMP/schema.json" --format >"$TMP/stderr.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The --format option requires --fix to be set +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" lint "$TMP/schema.json" --format --json >"$TMP/stdout.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +{ + "error": "The --format option requires --fix to be set" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/lint/fail_lint_format_yaml.sh b/test/lint/fail_lint_format_yaml.sh new file mode 100755 index 00000000..b534bfd1 --- /dev/null +++ b/test/lint/fail_lint_format_yaml.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.yaml" +$schema: http://json-schema.org/draft-06/schema# +type: string +EOF + +"$1" lint "$TMP/schema.yaml" --fix --format >"$TMP/stderr.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The --fix option is not supported for YAML input files + at file path $(realpath "$TMP/schema.yaml") +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" lint "$TMP/schema.yaml" --fix --format --json >"$TMP/stdout.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +{ + "error": "The --fix option is not supported for YAML input files", + "filePath": "$(realpath "$TMP/schema.yaml")" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/lint/fail_lint_keep_ordering_without_format.sh b/test/lint/fail_lint_keep_ordering_without_format.sh new file mode 100755 index 00000000..2a4b0f79 --- /dev/null +++ b/test/lint/fail_lint_keep_ordering_without_format.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "string" +} +EOF + +"$1" lint "$TMP/schema.json" --fix --keep-ordering >"$TMP/stderr.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +error: The --keep-ordering option requires --format to be set +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" + +# JSON error +"$1" lint "$TMP/schema.json" --fix --keep-ordering --json >"$TMP/stdout.txt" 2>&1 \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +{ + "error": "The --keep-ordering option requires --format to be set" +} +EOF + +diff "$TMP/stdout.txt" "$TMP/expected.txt" diff --git a/test/lint/pass_lint_fix_format.sh b/test/lint/pass_lint_fix_format.sh new file mode 100755 index 00000000..e9d23b86 --- /dev/null +++ b/test/lint/pass_lint_fix_format.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "title": "Test", + "description": "Test schema", + "type": "string", + "examples": [ "foo" ], + "$schema": "http://json-schema.org/draft-06/schema#", + "const": "foo" +} +EOF + +"$1" lint "$TMP/schema.json" --fix --format > "$TMP/output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected_output.txt" +EOF +diff "$TMP/output.txt" "$TMP/expected_output.txt" + +cat << 'EOF' > "$TMP/expected.json" +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Test", + "description": "Test schema", + "examples": [ "foo" ], + "const": "foo" +} +EOF + +diff "$TMP/schema.json" "$TMP/expected.json" diff --git a/test/lint/pass_lint_fix_format_directory.sh b/test/lint/pass_lint_fix_format_directory.sh new file mode 100755 index 00000000..0a7125e6 --- /dev/null +++ b/test/lint/pass_lint_fix_format_directory.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +mkdir -p "$TMP/schemas" + +cat << 'EOF' > "$TMP/schemas/schema1.json" +{ + "title": "Schema 1", + "description": "First schema", + "examples": [ "foo" ], + "type": "string", + "$schema": "http://json-schema.org/draft-06/schema#", + "const": "foo" +} +EOF + +cat << 'EOF' > "$TMP/schemas/schema2.json" +{ + "title": "Schema 2", + "description": "Second schema", + "examples": [ 42 ], + "type": "number", + "$schema": "http://json-schema.org/draft-06/schema#" +} +EOF + +"$1" lint "$TMP/schemas" --fix --format > "$TMP/output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected_output.txt" +EOF +diff "$TMP/output.txt" "$TMP/expected_output.txt" + +cat << 'EOF' > "$TMP/expected1.json" +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Schema 1", + "description": "First schema", + "examples": [ "foo" ], + "const": "foo" +} +EOF + +cat << 'EOF' > "$TMP/expected2.json" +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Schema 2", + "description": "Second schema", + "examples": [ 42 ], + "type": "number" +} +EOF + +diff "$TMP/schemas/schema1.json" "$TMP/expected1.json" +diff "$TMP/schemas/schema2.json" "$TMP/expected2.json" diff --git a/test/lint/pass_lint_fix_format_keep_ordering.sh b/test/lint/pass_lint_fix_format_keep_ordering.sh new file mode 100755 index 00000000..98e9b329 --- /dev/null +++ b/test/lint/pass_lint_fix_format_keep_ordering.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "title": "Test", + "description": "Test schema", + "type": "string", + "examples": [ "foo" ], + "$schema": "http://json-schema.org/draft-06/schema#", + "const": "foo" +} +EOF + +"$1" lint "$TMP/schema.json" --fix --format --keep-ordering > "$TMP/output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected_output.txt" +EOF +diff "$TMP/output.txt" "$TMP/expected_output.txt" + +cat << 'EOF' > "$TMP/expected.json" +{ + "title": "Test", + "description": "Test schema", + "examples": [ "foo" ], + "$schema": "http://json-schema.org/draft-06/schema#", + "const": "foo" +} +EOF + +diff "$TMP/schema.json" "$TMP/expected.json" diff --git a/test/lint/pass_lint_fix_format_no_issues.sh b/test/lint/pass_lint_fix_format_no_issues.sh new file mode 100755 index 00000000..e53d92b2 --- /dev/null +++ b/test/lint/pass_lint_fix_format_no_issues.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "title": "Test", + "description": "Test schema", + "examples": [ "foo" ], + "type": "string", + "$schema": "http://json-schema.org/draft-06/schema#" +} +EOF + +# First verify there are no lint issues +"$1" lint "$TMP/schema.json" > "$TMP/output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected_output.txt" +EOF +diff "$TMP/output.txt" "$TMP/expected_output.txt" + +# Now test that --fix --format still formats the file +"$1" lint "$TMP/schema.json" --fix --format > "$TMP/output.txt" 2>&1 + +cat << 'EOF' > "$TMP/expected_output.txt" +EOF +diff "$TMP/output.txt" "$TMP/expected_output.txt" + +cat << 'EOF' > "$TMP/expected.json" +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Test", + "description": "Test schema", + "examples": [ "foo" ], + "type": "string" +} +EOF + +diff "$TMP/schema.json" "$TMP/expected.json"