diff --git a/core/ast.h b/core/ast.h index 24a76fa2d..24a03efee 100644 --- a/core/ast.h +++ b/core/ast.h @@ -92,6 +92,8 @@ static inline std::string ASTTypeToString(ASTType type) { case AST_UNARY: return "AST_UNARY"; case AST_VAR: return "AST_VAR"; } + std::cerr << "Invalid AST type" << "\n"; + abort(); } /** Represents a variable / parameter / field name. */ diff --git a/core/formatter.cpp b/core/formatter.cpp index 67923271c..f231b0bb1 100644 --- a/core/formatter.cpp +++ b/core/formatter.cpp @@ -963,6 +963,280 @@ class PrettyFieldNames : public FmtPass { } }; +/// Add newlines inside complex structures (arrays, objects etc.). +/// +/// The main principle is that a structure can either be: +/// * expanded and contain newlines in all the designated places +/// * unexpanded and not contain any newlines in any of designated places. +/// +/// It only looks shallowly at the AST nodes, so there may be some newlines deeper that +/// don't affect expanding. For example: +/// [{ +/// 'a': 'b', +/// 'c': 'd', +/// }] +/// The outer array can be unexpanded, because there are no newlines between +class FixNewlines: public FmtPass { + using FmtPass::visit; + + bool shouldExpand(const Array *array) + { + for (const auto &elem: array->elements) { + if (countNewlines(open_fodder(elem.expr)) > 0) { + return true; + } + } + if (countNewlines(array->closeFodder) > 0) { + return true; + } + return false; + } + + void expand(Array *array) + { + for (auto &elem: array->elements) { + ensureCleanNewline(open_fodder(elem.expr)); + } + ensureCleanNewline(array->closeFodder); + } + + Fodder &objectFieldOpenFodder(ObjectField &field) + { + if (field.kind == ObjectField::Kind::FIELD_STR) { + return field.expr1->openFodder; + } + return field.fodder1; + } + + bool shouldExpand(Object *object) + { + for (auto &field: object->fields) { + if (countNewlines(objectFieldOpenFodder(field)) > 0) { + return true; + } + } + if (countNewlines(object->closeFodder) > 0) { + return true; + } + return false; + } + + void expand(Object *object) + { + for (auto &field: object->fields) { + ensureCleanNewline(objectFieldOpenFodder(field)); + } + ensureCleanNewline(object->closeFodder); + } + + bool shouldExpand(Local *local) + { + for (auto &bind: local->binds) { + if (countNewlines(bind.varFodder) > 0) { + return true; + } + } + return false; + } + + void expand(Local *local) + { + bool first = true; + for (auto &bind: local->binds) { + if (!first) { + ensureCleanNewline(bind.varFodder); + } + first = false; + } + } + + bool shouldExpand(ArrayComprehension *comp) + { + if (countNewlines(open_fodder(comp->body)) > 0) { + return true; + } + for (auto &spec: comp->specs) { + if (countNewlines(spec.openFodder) > 0) { + return true; + } + } + if (countNewlines(comp->closeFodder) > 0) { + return true; + } + return false; + } + + void expand(ArrayComprehension *comp) + { + ensureCleanNewline(open_fodder(comp->body)); + for (auto &spec: comp->specs) { + ensureCleanNewline(spec.openFodder); + } + ensureCleanNewline(comp->closeFodder); + } + + bool shouldExpand(ObjectComprehension *comp) + { + for (auto &field: comp->fields) { + if (countNewlines(objectFieldOpenFodder(field)) > 0) { + return true; + } + } + for (auto &spec: comp->specs) { + if (countNewlines(spec.openFodder) > 0) { + return true; + } + } + if (countNewlines(comp->closeFodder) > 0) { + return true; + } + return false; + } + + void expand(ObjectComprehension *comp) + { + for (auto &field: comp->fields) { + ensureCleanNewline(objectFieldOpenFodder(field)); + } + for (auto &spec: comp->specs) { + ensureCleanNewline(spec.openFodder); + } + ensureCleanNewline(comp->closeFodder); + } + + bool shouldExpand(Parens *parens) + { + return countNewlines(open_fodder(parens->expr)) > 0 + || countNewlines(parens->closeFodder) > 0; + } + + void expand(Parens *parens) + { + ensureCleanNewline(open_fodder(parens->expr)); + ensureCleanNewline(parens->closeFodder); + } + + Fodder &argParamOpenFodder(ArgParam ¶m) + { + if (param.id != nullptr) { + return param.idFodder; + } else if (param.expr != nullptr) { + return open_fodder(param.expr); + } else { + std::cerr << "Invalid ArgParam" << std::endl; + abort(); + } + } + + // Example: + // f(1, 2, + // 3) + // Should be expanded to: + // f(1, + // 2, + // 3) + bool shouldExpandBetween(ArgParams ¶ms) + { + bool first = true; + for (auto ¶m: params) { + if (!first && countNewlines(argParamOpenFodder(param)) > 0) { + return true; + } + first = false; + } + return false; + } + + void expandBetween(ArgParams ¶ms) + { + bool first = true; + for (auto ¶m: params) { + if (!first) { + ensureCleanNewline(argParamOpenFodder(param)); + } + first = false; + } + } + + // Example: + // very_long_function_on_a_long_line_so_arguments_dont_fit_on_the_same_line( + // 1, 2, 3) + // Should be expanded to: + // very_long_function_on_a_long_line_so_arguments_dont_fit_on_the_same_line( + // 1, 2, 3 + // ) + bool shouldExpandNearParens(ArgParams ¶ms, Fodder &fodder_r) + { + if (params.empty()) { + return false; + } + auto &argFodder = argParamOpenFodder(params.front()); + return countNewlines(fodder_r) > 0 || countNewlines(argFodder) > 0; + } + + void expandNearParens(ArgParams ¶ms, Fodder &fodder_r) + { + if (!params.empty()) { + ensureCleanNewline(argParamOpenFodder(params.front())); + } + ensureCleanNewline(fodder_r); + } + + public: + FixNewlines(Allocator &alloc, const FmtOpts &opts) : FmtPass(alloc, opts) { } + + template + void simpleExpandingVisit(T *expr) { + if (shouldExpand(expr)) { + expand(expr); + } + FmtPass::visit(expr); + } + + void visit(Array *array) + { + simpleExpandingVisit(array); + } + + void visit(Object *object) + { + simpleExpandingVisit(object); + } + + void visit(Local *local) + { + simpleExpandingVisit(local); + } + + void visit(ArrayComprehension *comp) + { + simpleExpandingVisit(comp); + } + + void visit(ObjectComprehension *comp) + { + simpleExpandingVisit(comp); + } + + void visit(Parens *parens) + { + simpleExpandingVisit(parens); + } + + void params(Fodder &fodder_l, ArgParams ¶ms, Fodder &fodder_r) + { + if (shouldExpandBetween(params)) { + expandBetween(params); + } + + if (shouldExpandNearParens(params, fodder_r)) { + expandNearParens(params, fodder_r); + } + + FmtPass::params(fodder_l, params, fodder_r); + } +}; + class FixIndentation { FmtOpts opts; @@ -977,7 +1251,7 @@ class FixIndentation { * \param separate_token If the last fodder was an interstitial, whether a space should follow * it. * \param all_but_last_indent New indentation value for all but final fodder element. - * \param last_indent New indentation value for but final fodder element. + * \param last_indent New indentation value for the final fodder element. */ void fill(Fodder &fodder, bool space_before, bool separate_token, unsigned all_but_last_indent, unsigned last_indent) @@ -1889,13 +2163,6 @@ class SortImports { return false; } - void ensureCleanNewline(Fodder &fodder) - { - if (!fodder_has_clean_endline(fodder)) { - fodder_push_back(fodder, FodderElement(FodderElement::Kind::LINE_END, 0, 0, {})); - } - } - AST *toplevelImport(Local *local, ImportElems &imports, const Fodder &groupOpenFodder) { assert(isGoodLocal(local)); @@ -1958,6 +2225,7 @@ std::string jsonnet_fmt(AST *ast, Fodder &final_fodder, const FmtOpts &opts) remove_initial_newlines(ast); if (opts.maxBlankLines > 0) EnforceMaximumBlankLines(alloc, opts).file(ast, final_fodder); + FixNewlines(alloc, opts).file(ast, final_fodder); FixTrailingCommas(alloc, opts).file(ast, final_fodder); FixParens(alloc, opts).file(ast, final_fodder); FixPlusObject(alloc, opts).file(ast, final_fodder); diff --git a/core/lexer.h b/core/lexer.h index 1837841be..aa61ee3cd 100644 --- a/core/lexer.h +++ b/core/lexer.h @@ -180,12 +180,42 @@ static inline void fodder_move_front(Fodder &a, Fodder &b) b.clear(); } -static inline Fodder make_fodder(const FodderElement &elem) { +static inline Fodder make_fodder(const FodderElement &elem) +{ Fodder fodder; fodder_push_back(fodder, elem); return fodder; } +static inline void ensureCleanNewline(Fodder &fodder) +{ + if (!fodder_has_clean_endline(fodder)) { + fodder_push_back(fodder, FodderElement(FodderElement::Kind::LINE_END, 0, 0, {})); + } +} + +static inline int countNewlines(const FodderElement &elem) +{ + switch (elem.kind) { + case FodderElement::INTERSTITIAL: + return 0; + case FodderElement::LINE_END: + return 1; + case FodderElement::PARAGRAPH: + return elem.comment.size() + elem.blanks; + } + std::cerr << "Unknown FodderElement kind" << std::endl; + abort(); +} + +static inline int countNewlines(const Fodder &fodder) +{ + int sum = 0; + for (const auto &elem: fodder) { + sum += countNewlines(elem); + } + return sum; +} static inline std::ostream &operator<<(std::ostream &o, const Fodder &fodder) { diff --git a/core/pass.cpp b/core/pass.cpp index ce274a54c..fe2404ecf 100644 --- a/core/pass.cpp +++ b/core/pass.cpp @@ -116,12 +116,7 @@ void CompilerPass::expr(AST *&ast_) void CompilerPass::visit(Apply *ast) { expr(ast->target); - fodder(ast->fodderL); - for (auto &arg : ast->args) { - expr(arg.expr); - fodder(arg.commaFodder); - } - fodder(ast->fodderR); + params(ast->fodderL, ast->args, ast->fodderR); if (ast->tailstrict) { fodder(ast->tailstrictFodder); } @@ -232,7 +227,7 @@ void CompilerPass::visit(Local *ast) fodder(bind.varFodder); if (bind.functionSugar) { params(bind.parenLeftFodder, bind.params, bind.parenRightFodder); - } + } fodder(bind.opFodder); expr(bind.body); fodder(bind.closeFodder); diff --git a/examples/bar_menu.5.jsonnet b/examples/bar_menu.5.jsonnet index 1e6fc2574..4c7a7e5f7 100644 --- a/examples/bar_menu.5.jsonnet +++ b/examples/bar_menu.5.jsonnet @@ -35,7 +35,8 @@ limitations under the License. ], garnish: null, served: "On The Rocks", - } for sd in [ + } + for sd in [ { name: "Yellow ", fruit: "Lemonade" }, { name: "", fruit: "Orange Juice" }, ] diff --git a/examples/bar_menu.7.jsonnet b/examples/bar_menu.7.jsonnet index 04e57e449..56a2d99e6 100644 --- a/examples/bar_menu.7.jsonnet +++ b/examples/bar_menu.7.jsonnet @@ -22,15 +22,20 @@ local utils = import "bar_menu_utils.libsonnet"; "Bee's Knees": { // Divide 4oz among the 3 ingredients. ingredients: utils.equal_parts(4, [ - "Honey Syrup", "Lemon Juice", my_gin]), + "Honey Syrup", + "Lemon Juice", + my_gin, + ]), garnish: "Lemon Twist", served: "Straight Up", }, Negroni: { // Divide 3oz among the 3 ingredients. ingredients: utils.equal_parts(3, [ - my_gin, "Sweet Red Vermouth", - "Campari"]), + my_gin, + "Sweet Red Vermouth", + "Campari", + ]), garnish: "Orange Peel", served: "On The Rocks", }, diff --git a/examples/bar_menu.8.jsonnet b/examples/bar_menu.8.jsonnet index 4308755ab..a06cfde09 100644 --- a/examples/bar_menu.8.jsonnet +++ b/examples/bar_menu.8.jsonnet @@ -21,10 +21,14 @@ limitations under the License. local neg = self, ingredients: [ { kind: "Farmers Gin", qty: 1 }, - { kind: "Sweet Red Vermouth", - qty: neg.ingredients[0].qty }, - { kind: "Campari", - qty: neg.ingredients[0].qty }, + { + kind: "Sweet Red Vermouth", + qty: neg.ingredients[0].qty, + }, + { + kind: "Campari", + qty: neg.ingredients[0].qty, + }, ], garnish: "Orange Peel", served: "On The Rocks", diff --git a/examples/bar_menu.8.jsonnet.golden b/examples/bar_menu.8.jsonnet.golden index 7db0e843a..e520d276d 100644 --- a/examples/bar_menu.8.jsonnet.golden +++ b/examples/bar_menu.8.jsonnet.golden @@ -1,12 +1,21 @@ { "cocktails": { "Negroni": { + "garnish": "Orange Peel", "ingredients": [ - { "kind": "Farmers Gin", "qty": 1 }, - { "kind": "Sweet Red Vermouth", "qty": 1 }, - { "kind": "Campari", "qty": 1 } + { + "kind": "Farmers Gin", + "qty": 1 + }, + { + "kind": "Sweet Red Vermouth", + "qty": 1 + }, + { + "kind": "Campari", + "qty": 1 + } ], - "garnish": "Orange Peel", "served": "On The Rocks" } } diff --git a/examples/terraform/aws-count.jsonnet b/examples/terraform/aws-count.jsonnet index a7b33049f..311810e00 100644 --- a/examples/terraform/aws-count.jsonnet +++ b/examples/terraform/aws-count.jsonnet @@ -51,7 +51,8 @@ local web_indexes = std.range(1, 4); // i is a scoped variable here instance_type: "m1.small", ami: aws_amis[aws_region], - } for i in web_indexes + } + for i in web_indexes }, }, diff --git a/test_suite/array.jsonnet.fmt.golden b/test_suite/array.jsonnet.fmt.golden index 42ad34276..f6349c205 100644 --- a/test_suite/array.jsonnet.fmt.golden +++ b/test_suite/array.jsonnet.fmt.golden @@ -41,8 +41,14 @@ std.assertEqual([x * x for x in []], []) && std.assertEqual(local x = 10; [x for x in [x, x, x]], [10, 10, 10]) && local arr = [ - { x: 1, y: 4, z: true }, { x: 1, y: 4, z: false }, { x: 1, y: 6, z: true }, { x: 1, y: 6, z: false }, - { x: 2, y: 6, z: true }, { x: 2, y: 6, z: false }, { x: 3, y: 6, z: true }, { x: 3, y: 6, z: false }, + { x: 1, y: 4, z: true }, + { x: 1, y: 4, z: false }, + { x: 1, y: 6, z: true }, + { x: 1, y: 6, z: false }, + { x: 2, y: 6, z: true }, + { x: 2, y: 6, z: false }, + { x: 3, y: 6, z: true }, + { x: 3, y: 6, z: false }, ]; std.assertEqual(arr, [{ x: x, y: y, z: z } for x in [1, 2, 3] for y in [1, 4, 6] if x + 2 < y for z in [true, false]]) && diff --git a/test_suite/formatter.jsonnet.fmt.golden b/test_suite/formatter.jsonnet.fmt.golden index 858bfdf4b..5bf32d47b 100644 --- a/test_suite/formatter.jsonnet.fmt.golden +++ b/test_suite/formatter.jsonnet.fmt.golden @@ -32,7 +32,8 @@ limitations under the License. }, local test_local4( - x, y) = { + x, y + ) = { g: 2, }, @@ -47,7 +48,8 @@ limitations under the License. local test_local_default2( x=100, - y=200) + y=200 + ) = { g: 2, }, @@ -64,13 +66,15 @@ limitations under the License. g: 1, }, - test_field0B: { f: 1, - g: 1, + test_field0B: { + f: 1, + g: 1, }, test_field1B: - { f: 1, - g: 1, + { + f: 1, + g: 1, }, test_field2: if true then @@ -149,7 +153,8 @@ limitations under the License. test_field9a: g( { f: 3, - }), + } + ), test_field9b: g( { @@ -162,10 +167,11 @@ limitations under the License. f: 3, }), - test_field9d: f(10, - { - f: 3, - } + test_field9d: f( + 10, + { + f: 3, + } ), test_field9e: f(local x = 10; @@ -207,8 +213,9 @@ limitations under the License. 1, ], - test_field13: [1, - 2, + test_field13: [ + 1, + 2, ], test_field14(obj):: if 10000 == 10000 then "wheeeeeeeee" @@ -318,28 +325,32 @@ limitations under the License. ["baz"] else [], - fox: std.equals(["foo"], - if true then [ - "baz", - ] else [], + fox: std.equals( + ["foo"], + if true then [ + "baz", + ] else [], ), - bax: std.equals(["foo"], - if true then - ["baz"] - else [], + bax: std.equals( + ["foo"], + if true then + ["baz"] + else [], ), - foy: [["foo"], - if true then [ - "baz", - ] else [], + foy: [ + ["foo"], + if true then [ + "baz", + ] else [], ], - bay: [["foo"], - if true then - ["baz"] - else [], + bay: [ + ["foo"], + if true then + ["baz"] + else [], ], foz: (["foo"] diff --git a/test_suite/formatting_braces.jsonnet b/test_suite/formatting_braces.jsonnet new file mode 100644 index 000000000..ab7f3c6d8 --- /dev/null +++ b/test_suite/formatting_braces.jsonnet @@ -0,0 +1,14 @@ +// See https://github.com/google/jsonnet/issues/230 +{ + volumeMounts: [ + { + name: "deployment-config", + mountPath: + if 42 == 42 then + "/box/deployment-config" + else + "/box" + , + }, + ], +} diff --git a/test_suite/formatting_braces.jsonnet.fmt.golden b/test_suite/formatting_braces.jsonnet.fmt.golden new file mode 100644 index 000000000..b5df1b613 --- /dev/null +++ b/test_suite/formatting_braces.jsonnet.fmt.golden @@ -0,0 +1,13 @@ +// See https://github.com/google/jsonnet/issues/230 +{ + volumeMounts: [ + { + name: "deployment-config", + mountPath: + if 42 == 42 then + "/box/deployment-config" + else + "/box", + }, + ], +} diff --git a/test_suite/formatting_braces.jsonnet.golden b/test_suite/formatting_braces.jsonnet.golden new file mode 100644 index 000000000..790423a16 --- /dev/null +++ b/test_suite/formatting_braces.jsonnet.golden @@ -0,0 +1,8 @@ +{ + "volumeMounts": [ + { + "mountPath": "/box/deployment-config", + "name": "deployment-config" + } + ] +} diff --git a/test_suite/formatting_braces2.jsonnet b/test_suite/formatting_braces2.jsonnet new file mode 100644 index 000000000..b65fdeea6 --- /dev/null +++ b/test_suite/formatting_braces2.jsonnet @@ -0,0 +1,48 @@ +// See: https://github.com/google/jsonnet/issues/299 +{ + someOther():: 42, + collCorrect1: [{ + name: "Good", + }], + collCorrect2: [{ + name: "Bad", + }, self.someOther(), + ], + collCorrect2a: [{ + name: "Bad", + }, self.someOther(),], + collCorrect2b: [ + { + name: "Bad", + }, self.someOther(),], + collCorrect2c: [ + { + name: "Bad", + }, self.someOther(),], + collCorrect2d: [ + { + name: "Bad", + }, + self.someOther(),], + collCorrect2e: [ + { + name: "Bad", + }, + self.someOther(), + ], + collCorrect3: { a: { + }}, + collCorrect3a: { a: { + } + }, + collWeird: [{ + name: "Bad", + }, + self.someOther(), + ], + collWeird2: [{ + name: "Bad", + }, + self.someOther(), + ], +} diff --git a/test_suite/formatting_braces2.jsonnet.fmt.golden b/test_suite/formatting_braces2.jsonnet.fmt.golden new file mode 100644 index 000000000..a30ceb4ed --- /dev/null +++ b/test_suite/formatting_braces2.jsonnet.fmt.golden @@ -0,0 +1,58 @@ +// See: https://github.com/google/jsonnet/issues/299 +{ + someOther():: 42, + collCorrect1: [{ + name: "Good", + }], + collCorrect2: [ + { + name: "Bad", + }, + self.someOther(), + ], + collCorrect2a: [{ + name: "Bad", + }, self.someOther()], + collCorrect2b: [ + { + name: "Bad", + }, + self.someOther(), + ], + collCorrect2c: [ + { + name: "Bad", + }, + self.someOther(), + ], + collCorrect2d: [ + { + name: "Bad", + }, + self.someOther(), + ], + collCorrect2e: [ + { + name: "Bad", + }, + self.someOther(), + ], + collCorrect3: { a: { + } }, + collCorrect3a: { + a: { + }, + }, + collWeird: [ + { + name: "Bad", + }, + self.someOther(), + ], + collWeird2: [ + { + name: "Bad", + }, + self.someOther(), + ], +} diff --git a/test_suite/formatting_braces2.jsonnet.golden b/test_suite/formatting_braces2.jsonnet.golden new file mode 100644 index 000000000..b8f5c45dc --- /dev/null +++ b/test_suite/formatting_braces2.jsonnet.golden @@ -0,0 +1,61 @@ +{ + "collCorrect1": [ + { + "name": "Good" + } + ], + "collCorrect2": [ + { + "name": "Bad" + }, + 42 + ], + "collCorrect2a": [ + { + "name": "Bad" + }, + 42 + ], + "collCorrect2b": [ + { + "name": "Bad" + }, + 42 + ], + "collCorrect2c": [ + { + "name": "Bad" + }, + 42 + ], + "collCorrect2d": [ + { + "name": "Bad" + }, + 42 + ], + "collCorrect2e": [ + { + "name": "Bad" + }, + 42 + ], + "collCorrect3": { + "a": { } + }, + "collCorrect3a": { + "a": { } + }, + "collWeird": [ + { + "name": "Bad" + }, + 42 + ], + "collWeird2": [ + { + "name": "Bad" + }, + 42 + ] +} diff --git a/test_suite/formatting_braces3.jsonnet b/test_suite/formatting_braces3.jsonnet new file mode 100644 index 000000000..218755bbf --- /dev/null +++ b/test_suite/formatting_braces3.jsonnet @@ -0,0 +1,351 @@ +local f(a, b) = 42; // useful for further tests +local x = [ + // ---------------------- local + + local y = "blah", + z = "blah"; + + local y = "blah", z = "blah"; + + local y = "blah", z = "blah", + x = "blah"; + + local + y = "blah", + z = "blah"; + + // ---------------------- arrays + + [], + + [ + ], + + [ 1, 2, 3], + + [ + 1, 2, 3], + + [ 1, 2, + 3], + + [ 1, 2, 3 + ], + + [ 1, 2 // before comma + , 3], + + + // ---------------------- objects + + {}, + + { + }, + + { 'a': 'b', 'c': 'd'}, + + // make sure it stays a string + { + 'a ': 'b', 'c': 'd'}, + + { + ['a' + ' ']: 'b', 'c': 'd'}, + + { + ['a' + ' ']: 'b', 'c': 'd'}, + + { + assert 42 == 42, 'a': 'b', 'c': 'd'}, + + { + 'a':: 'b', 'c': 'd'}, + + { 'a': 'b', + 'c': 'd'}, + + { 'a': 'b', 'c': 'd' + }, + + { 'a': + 'b', 'c': 'd'}, + + { 'a': 'b' // before comma + , 'c': 'd'}, + + + // ---------------------- array comprehensions + + [ 42 for x in [1] if x == 1], + + [ 42 for x in [1] if x == 1], + + [ 42 + for x in [1] if x == 1], + + [ 42 + for x in [1] + if x == 1], + + [ 42 + for x in [1] if x == 1 + if x == 1], + + [ 42 for x in [1] if x == 1 + ], + + [ 42 for x in [1] if + x == 1], + + // ---------------------- object comprehensions + + { [x]: 42 for x in [1] if x == 1}, + + { + [x]: 42 for x in [1] if x == 1}, + + { [x]: 42 + for x in [1] if x == 1}, + + { [x]: 42 for x in [1] + if x == 1}, + + { [x]: 42 for x in [1] if x == 1 + }, + + { [x]: 42 for x in [1] if + x == 1}, + + // ---------------------- functions + + function(x, y) 42, + function(x, + y) 42, + function( + x, y) 42, + function(x, y + ) 42, + function(x, y) + 42, + function(x, y) 42, + + local f(x, y) = 42; + + // TODO(sbarzowski) some weird lining up going on here + local f(x, y + ) = 42; + local f( + x, y) = 42; + + local f(x, + y) = 42; + + local f(x // before comma + , y) = 42; + + local f(x, y) = + 42; + + local f(x, y) + = 42; + + { f(x, y, z)::42 }, + + { f(x, y, + z)::42 }, + + // ---------------------- function calls + + f(1, 2), + + f( + 1, 2), + + f + (1, 2), + + f(1, + 2), + + f(1, 2 + ), + + f(1 # before comma + , 2), + + // ---------------------- parens + + (1 + 2 + 3), + + (1 + 2 + + 3), + + (1 + 2 + 3 + ), + + ( + 1 + 2 + 3), + + ( if 42 == 42 then 42 else 1337 ), + + ( if 42 == 42 + then 42 else 1337 ) + + ( + if 42 == 42 then 42 else 1337 ) + + ( if 42 == 42 then 42 else 1337 + ) + + ( + if 42 == 42 + then 42 + else 1337 + ) + + // ---------------------- indexing & slicing + + {}.a, + + {}. + a, + + {} + .a, + + {}['a '], + + {}[ + 'a '], + + {}['a ' + ], + + {} + ['a '], + + {}['a '], + + [][::], + + [][ + ::], + + [][:: + ], + + [][1:2:3], + + [][ + 1:2:3], + + [][1: + 2:3], + + [][1:2 + :3], + + [][1:2: + 3], + + [][1:2:3 + ], + + [][1:2], + + [][42 + + 42:2], + + [][ + 1:2], + + [][1 + :2], + + [][1: + 2], + + [][1:2 + ], + + // ---------------------- various content combinations + + [{}, {}], + + [ { + 'a': 'b' + }, { + 'a': 'b' + }, { + 'a': 'b' + }], + + [ {'a': 'b'}, { + 'a': 'b' + }, { + 'a': 'b' + }], + + [ { + 'a': 'b' + }, { + 'a': 'b' + }, { + 'a': 'b' + }, if 42==42 then + {'a': 'b'} + else + {'b': 'a'}], + + [ { + 'a': 'b' + }, { + 'a': 'b' + }, { + 'a': 'b' + }, + if 42==42 then + {'a': 'b'} + else + {'b': 'a'}], + + [ { + 'a': 'b' + }, { + 'a': 'b' + }, { + 'a': 'b' + }, if 42==42 then + {'a': 'b'} + else + {'b': 'a'} + ], + + [ {'a': 'b'}, { + 'a': 'b' + }, { + 'a': 'b' + }], + + [ {'a': 'b'}, {'a': 'b'}, {'a': 'b'} ], + + [ {'a': 'b'}, {'a': 'b'}, {'a': 'b'}, if 42==42 then {'a': 'b'} else {'b': 'a'}], + + [ {'this': + 'is', 'a': + 'really', complex: 'expression'} for x in [1] ], + + [ { 'a': 'b' }, 1, if 42 == 42 then 42 else 1337, 17], + + [ { 'a': 'b' }, 1, if 42 == 42 then 42 else 1337 # before comma + , 17], + + [ { 'a': 'b' }, 1, if 42 == 42 then 42 else 1337, # after comma + 17], + + { 'a': [ 'b', 'c' ]}, + + { 'a': [ 'b', 'c' + ]}, +]; +true diff --git a/test_suite/formatting_braces3.jsonnet.fmt.golden b/test_suite/formatting_braces3.jsonnet.fmt.golden new file mode 100644 index 000000000..a588bf2e7 --- /dev/null +++ b/test_suite/formatting_braces3.jsonnet.fmt.golden @@ -0,0 +1,424 @@ +local f(a, b) = 42; // useful for further tests +local x = [ + // ---------------------- local + + local y = "blah", + z = "blah"; + + local y = "blah", z = "blah"; + + local y = "blah", + z = "blah", + x = "blah"; + + local + y = "blah", + z = "blah"; + + // ---------------------- arrays + + [], + + [ + ], + + [1, 2, 3], + + [ + 1, + 2, + 3, + ], + + [ + 1, + 2, + 3, + ], + + [ + 1, + 2, + 3, + ], + + [1, 2 // before comma + , 3], + + + // ---------------------- objects + + {}, + + { + }, + + { a: 'b', c: 'd' }, + + // make sure it stays a string + { + 'a ': 'b', + c: 'd', + }, + + { + ['a' + ' ']: 'b', + c: 'd', + }, + + { + ['a' + ' ']: 'b', + c: 'd', + }, + + { + assert 42 == 42, + a: 'b', + c: 'd', + }, + + { + a:: 'b', + c: 'd', + }, + + { + a: 'b', + c: 'd', + }, + + { + a: 'b', + c: 'd', + }, + + { a: + 'b', c: 'd' }, + + { a: 'b' // before comma + , c: 'd' }, + + + // ---------------------- array comprehensions + + [42 for x in [1] if x == 1], + + [42 for x in [1] if x == 1], + + [ + 42 + for x in [1] + if x == 1 + ], + + [ + 42 + for x in [1] + if x == 1 + ], + + [ + 42 + for x in [1] + if x == 1 + if x == 1 + ], + + [ + 42 + for x in [1] + if x == 1 + ], + + [42 for x in [1] if + x == 1], + + // ---------------------- object comprehensions + + { [x]: 42 for x in [1] if x == 1 }, + + { + [x]: 42 + for x in [1] + if x == 1 + }, + + { + [x]: 42 + for x in [1] + if x == 1 + }, + + { + [x]: 42 + for x in [1] + if x == 1 + }, + + { + [x]: 42 + for x in [1] + if x == 1 + }, + + { [x]: 42 for x in [1] if + x == 1 }, + + // ---------------------- functions + + function(x, y) 42, + function(x, + y) 42, + function( + x, y + ) 42, + function( + x, y + ) 42, + function(x, y) + 42, + function(x, y) 42, + + local f(x, y) = 42; + + // TODO(sbarzowski) some weird lining up going on here + local f( + x, y + ) = 42; + local f( + x, y + ) = 42; + + local f(x, + y) = 42; + + local f(x // before comma + , y) = 42; + + local f(x, y) = + 42; + + local f(x, y) + = 42; + + { f(x, y, z):: 42 }, + + { f(x, + y, + z):: 42 }, + + // ---------------------- function calls + + f(1, 2), + + f( + 1, 2 + ), + + f + (1, 2), + + f(1, + 2), + + f( + 1, 2 + ), + + f(1 # before comma + , 2), + + // ---------------------- parens + + (1 + 2 + 3), + + (1 + 2 + + 3), + + ( + 1 + 2 + 3 + ), + + ( + 1 + 2 + 3 + ), + + (if 42 == 42 then 42 else 1337), + + (if 42 == 42 + then 42 else 1337) + + ( + if 42 == 42 then 42 else 1337 + ) + + ( + if 42 == 42 then 42 else 1337 + ) + + ( + if 42 == 42 + then 42 + else 1337 + ) + + // ---------------------- indexing & slicing + + {}.a, + + {}. + a, + + {} + .a, + + {}['a '], + + {}[ + 'a '], + + {}['a ' + ], + + {} + ['a '], + + {}['a '], + + [][:], + + [][ + :], + + [][: + ], + + [][1:2:3], + + [][ + 1:2:3], + + [][1: + 2:3], + + [][1:2 + :3], + + [][1:2: + 3], + + [][1:2:3 + ], + + [][1:2], + + [][42 + + 42:2], + + [][ + 1:2], + + [][1 + :2], + + [][1: + 2], + + [][1:2 + ], + + // ---------------------- various content combinations + + [{}, {}], + + [{ + a: 'b', + }, { + a: 'b', + }, { + a: 'b', + }], + + [{ a: 'b' }, { + a: 'b', + }, { + a: 'b', + }], + + [{ + a: 'b', + }, { + a: 'b', + }, { + a: 'b', + }, if 42 == 42 then + { a: 'b' } + else + { b: 'a' }], + + [ + { + a: 'b', + }, + { + a: 'b', + }, + { + a: 'b', + }, + if 42 == 42 then + { a: 'b' } + else + { b: 'a' }, + ], + + [ + { + a: 'b', + }, + { + a: 'b', + }, + { + a: 'b', + }, + if 42 == 42 then + { a: 'b' } + else + { b: 'a' }, + ], + + [{ a: 'b' }, { + a: 'b', + }, { + a: 'b', + }], + + [{ a: 'b' }, { a: 'b' }, { a: 'b' }], + + [{ a: 'b' }, { a: 'b' }, { a: 'b' }, if 42 == 42 then { a: 'b' } else { b: 'a' }], + + [{ this: + 'is', a: + 'really', complex: 'expression' } for x in [1]], + + [{ a: 'b' }, 1, if 42 == 42 then 42 else 1337, 17], + + [{ a: 'b' }, 1, if 42 == 42 then 42 else 1337 # before comma + , 17], + + [ + { a: 'b' }, + 1, + if 42 == 42 then 42 else 1337, # after comma + 17, + ], + + { a: ['b', 'c'] }, + + { a: [ + 'b', + 'c', + ] }, +]; +true diff --git a/test_suite/merge.jsonnet.fmt.golden b/test_suite/merge.jsonnet.fmt.golden new file mode 100644 index 000000000..5b3899860 --- /dev/null +++ b/test_suite/merge.jsonnet.fmt.golden @@ -0,0 +1,98 @@ +/* +Copyright 2016 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +# These cases are shamelessly lifted from +# https://tools.ietf.org/html/draft-ietf-appsawg-json-merge-patch-07#appendix-A +local cases = [ + { + target: { a: "b" }, + patch: { a: "c" }, + expect: { a: "c" }, + }, + { + target: { a: "b" }, + patch: { b: "c" }, + expect: { a: "b", b: "c" }, + }, + { + target: { a: "b" }, + patch: { a: null }, + expect: {}, + }, + { + target: { a: "b", b: "c" }, + patch: { a: null }, + expect: { b: "c" }, + }, + { + target: { a: ["b"] }, + patch: { a: "c" }, + expect: { a: "c" }, + }, + { + target: { a: "c" }, + patch: { a: ["b"] }, + expect: { a: ["b"] }, + }, + { + target: { a: { b: "c" } }, + patch: { a: [1] }, + expect: { a: [1] }, + }, + { + target: ["a", "b"], + patch: ["c", "d"], + expect: ["c", "d"], + }, + { + target: { a: "b" }, + patch: ["c"], + expect: ["c"], + }, + { + target: { a: "foo" }, + patch: null, + expect: null, + }, + { + target: { a: "foo" }, + patch: "bar", + expect: "bar", + }, + { + target: { e: null }, + patch: { a: 1 }, + expect: { e: null, a: 1 }, + }, + { + target: [1, 2], + patch: { a: "b", c: null }, + expect: { a: "b" }, + }, + { + target: {}, + patch: { a: { bb: { ccc: null } } }, + expect: { a: { bb: {} } }, + }, +]; + +local results = + [ + std.assertEqual(std.mergePatch(case.target, case.patch), case.expect) + for case in cases + ]; + +std.foldl(function(a, b) a && b, results, true) diff --git a/test_suite/object.jsonnet.fmt.golden b/test_suite/object.jsonnet.fmt.golden index 7c2a5daf3..987088663 100644 --- a/test_suite/object.jsonnet.fmt.golden +++ b/test_suite/object.jsonnet.fmt.golden @@ -62,8 +62,14 @@ std.assertEqual({ [x + ""]: x + foo, local foo = 3 for x in [1, 2, 3] }, { "1": local obj = { - f14true: { x: 1, y: 4, z: true }, f14false: { x: 1, y: 4, z: false }, f16true: { x: 1, y: 6, z: true }, f16false: { x: 1, y: 6, z: false }, - f26true: { x: 2, y: 6, z: true }, f26false: { x: 2, y: 6, z: false }, f36true: { x: 3, y: 6, z: true }, f36false: { x: 3, y: 6, z: false }, + f14true: { x: 1, y: 4, z: true }, + f14false: { x: 1, y: 4, z: false }, + f16true: { x: 1, y: 6, z: true }, + f16false: { x: 1, y: 6, z: false }, + f26true: { x: 2, y: 6, z: true }, + f26false: { x: 2, y: 6, z: false }, + f36true: { x: 3, y: 6, z: true }, + f36false: { x: 3, y: 6, z: false }, }; std.assertEqual(obj, { ["f" + x + y + z]: { x: x, y: y, z: z } for x in [1, 2, 3] for y in [1, 4, 6] if x + 2 < y for z in [true, false] }) && @@ -86,7 +92,8 @@ std.assertEqual({ f+: [3] }, { f: [3] }) && // into a different object scope: std.assertEqual( { opt:: true, f: { y: 5 } } + { f+: { [if "opt" in super then "x" else "y"]+: 3 } }, - { f: { x: 3, y: 5 } }) && + { f: { x: 3, y: 5 } } +) && std.assertEqual({ x: 1 } + { a: "x" in super, b: "y" in super }, { x: 1, a: true, b: false }) && std.assertEqual({ x:: 1 } + { a: "x" in super, b: "y" in super }, { a: true, b: false }) && diff --git a/test_suite/refresh_fmt_golden.sh b/test_suite/refresh_fmt_golden.sh index 859588d22..b2809e582 100755 --- a/test_suite/refresh_fmt_golden.sh +++ b/test_suite/refresh_fmt_golden.sh @@ -14,12 +14,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +set -e + if [ $# -eq 0 ] ; then echo "Usage: $0 " 2>&1 exit 1 fi for FILE in "$@" ; do + if [[ $FILE == *.golden ]]; then + echo "Specified file $FILE is already golden." + echo "Please, specify the input file instead." + exit 1 + fi + if [ ! -r "$FILE" ] ; then echo "Could not read: \"$FILE\"" 2>&1 exit 1 diff --git a/test_suite/refresh_golden.sh b/test_suite/refresh_golden.sh index 5896ef08b..5bb9901ae 100755 --- a/test_suite/refresh_golden.sh +++ b/test_suite/refresh_golden.sh @@ -14,12 +14,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +set -e + if [ $# -eq 0 ] ; then echo "Usage: $0 " 2>&1 exit 1 fi for FILE in "$@" ; do + if [[ $FILE == *.golden ]]; then + echo "Specified file $FILE is already golden." + echo "Please, specify the input file instead." + exit 1 + fi + if [ ! -r "$FILE" ] ; then echo "Could not read: \"$FILE\"" 2>&1 exit 1 diff --git a/test_suite/stdlib.jsonnet b/test_suite/stdlib.jsonnet index 2b1ec990b..02f4ea30f 100644 --- a/test_suite/stdlib.jsonnet +++ b/test_suite/stdlib.jsonnet @@ -24,7 +24,8 @@ std.assertEqual( local std = { sqrt: function(x) x }; local lib = import "lib/capture_std.libsonnet"; lib.sqrt(4), - 2) && + 2 +) && // Now, test each std library function in turn. @@ -195,7 +196,8 @@ std.assertEqual( empty: {}, }, }), - "a = 1\nb = 2\n[empty]\n[s1]\nx = 11\ny = 22\nz = 33\n[s2]\np = yes\nq = \n") && + "a = 1\nb = 2\n[empty]\n[s1]\nx = 11\ny = 22\nz = 33\n[s2]\np = yes\nq = \n" +) && std.assertEqual( std.manifestIni({ @@ -205,7 +207,8 @@ std.assertEqual( empty: {}, }, }), - "[empty]\n[s1]\nx = 11\ny = 22\nz = 33\n[s2]\np = yes\nq = \n") && + "[empty]\n[s1]\nx = 11\ny = 22\nz = 33\n[s2]\np = yes\nq = \n" +) && std.assertEqual(std.escapeStringJson("hello"), "\"hello\"") && std.assertEqual(std.escapeStringJson("he\"llo"), "\"he\\\"llo\"") && @@ -272,7 +275,8 @@ std.assertEqual(std.sort(["1", "2"]), ["1", "2"]) && std.assertEqual(std.sort(["2", "1"]), ["1", "2"]) && std.assertEqual( std.sort(["The", "rain", "in", "spain", "falls", "mainly", "on", "the", "plain."]), - ["The", "falls", "in", "mainly", "on", "plain.", "rain", "spain", "the"]) && + ["The", "falls", "in", "mainly", "on", "plain.", "rain", "spain", "the"] +) && std.assertEqual(std.uniq([]), []) && std.assertEqual(std.uniq([1]), [1]) && @@ -280,17 +284,20 @@ std.assertEqual(std.uniq([1, 2]), [1, 2]) && std.assertEqual(std.uniq(["1", "2"]), ["1", "2"]) && std.assertEqual( std.uniq(["The", "falls", "in", "mainly", "on", "plain.", "rain", "spain", "the"]), - ["The", "falls", "in", "mainly", "on", "plain.", "rain", "spain", "the"]) && + ["The", "falls", "in", "mainly", "on", "plain.", "rain", "spain", "the"] +) && local animal_set = ["ant", "bat", "cat", "dog", "elephant", "fish", "giraffe"]; std.assertEqual( std.uniq(["ant", "bat", "cat", "dog", "dog", "elephant", "fish", "fish", "giraffe"]), - animal_set) && + animal_set +) && std.assertEqual( std.set(["dog", "ant", "bat", "cat", "dog", "elephant", "fish", "giraffe", "fish"]), - animal_set) && + animal_set +) && std.assertEqual(std.setUnion(animal_set, animal_set), animal_set) && std.assertEqual(std.setUnion(animal_set, []), animal_set) && @@ -338,38 +345,40 @@ std.assertEqual(std.split("/foo/", "/"), ["", "foo", ""]) && std.assertEqual(std.splitLimit("foo/bar", "/", 1), ["foo", "bar"]) && std.assertEqual(std.splitLimit("/foo/", "/", 1), ["", "foo/"]) && -std.assertEqual(std.manifestJsonEx({ - x: [1, 2, 3, true, false, null, "string\nstring"], - y: { a: 1, b: 2, c: [1, 2] }, - emptyArray: [], - emptyObject: {}, -}, " ") + "\n", ||| - { - "emptyArray": [ - - ], - "emptyObject": { - - }, - "x": [ - 1, - 2, - 3, - true, - false, - null, - "string\nstring" - ], - "y": { - "a": 1, - "b": 2, - "c": [ +std.assertEqual( + std.manifestJsonEx({ + x: [1, 2, 3, true, false, null, "string\nstring"], + y: { a: 1, b: 2, c: [1, 2] }, + emptyArray: [], + emptyObject: {}, + }, " ") + "\n", + ||| + { + "emptyArray": [ + + ], + "emptyObject": { + + }, + "x": [ 1, - 2 - ] + 2, + 3, + true, + false, + null, + "string\nstring" + ], + "y": { + "a": 1, + "b": 2, + "c": [ + 1, + 2 + ] + } } - } -||| + ||| ) && std.assertEqual(std.parseInt("01234567890"), 1234567890) && diff --git a/test_suite/tests.source b/test_suite/tests.source index d5199a652..6840c5055 100644 --- a/test_suite/tests.source +++ b/test_suite/tests.source @@ -21,6 +21,9 @@ FAILED=0 # Display successful as well as failed tests (useful if they are slow). VERBOSE=false +separator() { + echo -e "\n-----------------------------\n" +} test_eval() { JSONNET_CMD="$1" @@ -39,6 +42,7 @@ test_eval() { echo "This run's output:" echo "$TEST_OUTPUT" echo "Actual exit code $TEST_EXIT_CODE, expected $EXPECTED_EXIT_CODE" + separator return 1 fi @@ -49,6 +53,7 @@ test_eval() { echo -e "\e[31;1mFAIL\e[0m \e[1m(regex mismatch)\e[0m: \e[36m$JSONNET_FILE\e[0m" echo "This run's output:" echo "$TEST_OUTPUT" + separator return 1 fi ;; @@ -61,6 +66,15 @@ test_eval() { echo "$TEST_OUTPUT" echo "Expected:" echo "$GOLDEN_OUTPUT" + echo "Diff:" + echo "$TEST_OUTPUT" > /tmp/jsonnet_output + echo "$GOLDEN_OUTPUT" > /tmp/jsonnet_golden + # Using git diff for pretty format and pretty colors + git --no-pager diff /tmp/jsonnet_golden /tmp/jsonnet_output + # The output is often quite long, let's repeat the filename once more + # to avoid wasting time on looking for it + echo -e "\nTEST FILE ABOVE: \e[36m$JSONNET_FILE\033[0m" + separator return 1 fi ;;