Repository: eslint/eslint (js)

Issue:
[Feature] Allow ignoring paths via package.json
I propose adding a second top level key to `package.json` named `eslintIgnore` that contains an array of paths to be ignored (or not).

 - #698 landed support for `eslintConfig` in `package.json` and it help keep the root of the project tidy.
 - #4261 proposed merging `.eslintignore` with `.eslintrc`, but it was rejected because "config files cascade, and ignore files do not".

Pull request:
New: Add eslintIgnore support to package.json (fixes #8458)
**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[ ] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[] Add something to the core
[X] Other, please explain: Add option to add ignore paths/fles to package.json with an eslintIgnore attribute

See #8458.

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (http://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**


**Is there anything you'd like reviewers to focus on?**




Patch:
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index 03a1dd794..f48670896 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -147,6 +147,37 @@ console.log(code.text);     // "var foo = bar;"
 
 In this way, you can retrieve the text and AST used for the last run of `linter.verify()`.
 
+### verifyAndFix()
+
+This method is similar to verify except that it also runs autofixing logic, similar to the `--fix` flag on the command line. The result object will contain the autofixed code, along with any remaining linting messages for the code that were not autofixed.
+
+```js
+var Linter = require("eslint").Linter;
+var linter = new Linter();
+
+var messages = linter.verifyAndFix("var foo", {
+    rules: {
+        semi: 2
+    }
+}, { filename: "foo.js" });
+```
+
+Output object from this method:
+
+```js
+{
+    fixed: true,
+    text: "var foo;",
+    messages: []
+}
+```
+
+The information available is:
+
+* `fixed` - True, if the code was fixed.
+* `text` - Fixed code text (might be the same as input if no fixes were applied).
+* `messages` - Collection of all messages for the given code (It has the same information as explained above under `verify` block).
+
 ## linter
 
 The `eslint.linter` object (deprecated) is an instance of the `Linter` class as defined [above](#Linter). `eslint.linter` exists for backwards compatibility, but we do not recommend using it because any mutations to it are shared among every module that uses `eslint`. Instead, please create your own instance of `eslint.Linter`.
diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md
index 3f59e8672..86469a5af 100644
--- a/docs/user-guide/configuring.md
+++ b/docs/user-guide/configuring.md
@@ -775,6 +775,22 @@ You can also use your `.gitignore` file:
 
 Any file that follows the standard ignore file format can be used. Keep in mind that specifying `--ignore-path` means that any existing `.eslintignore` file will not be used. Note that globbing rules in `.eslintignore` follow those of `.gitignore`.
 
+### Using eslintIgnore in package.json
+
+If an `.eslintignore` file is not found and an alternate file is not specified, eslint will look in package.json for an `eslintIgnore` key to check for files to ignore.
+
+    {
+      "name": "mypackage",
+      "version": "0.0.1",
+      "eslintConfig": {
+          "env": {
+              "browser": true,
+              "node": true
+          }
+      },
+      "eslintIgnore": ["hello.js", "world.js"]
+    }
+
 ### Ignored File Warnings
 
 When you pass directories to ESLint, files and directories are silently ignored. If you pass a specific file to ESLint, then you will see a warning indicating that the file was skipped. For example, suppose you have an `.eslintignore` file that looks like this:
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index d0abc0a55..f53c2e76a 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -23,11 +23,9 @@ const fs = require("fs"),
     Config = require("./config"),
     fileEntryCache = require("file-entry-cache"),
     globUtil = require("./util/glob-util"),
-    SourceCodeFixer = require("./util/source-code-fixer"),
     validator = require("./config/config-validator"),
     stringify = require("json-stable-stringify"),
     hash = require("./util/hash"),
-
     pkg = require("../package.json");
 
 const debug = require("debug")("eslint:cli-engine");
@@ -132,80 +130,6 @@ function calculateStatsPerRun(results) {
     });
 }
 
-/**
- * Performs multiple autofix passes over the text until as many fixes as possible
- * have been applied.
- * @param {string} text The source text to apply fixes to.
- * @param {Object} config The ESLint config object to use.
- * @param {Object} options The ESLint options object to use.
- * @param {string} options.filename The filename from which the text was read.
- * @param {boolean} options.allowInlineConfig Flag indicating if inline comments
- *      should be allowed.
- * @param {Linter} linter Linter context
- * @returns {Object} The result of the fix operation as returned from the
- *      SourceCodeFixer.
- * @private
- */
-function multipassFix(text, config, options, linter) {
-    const MAX_PASSES = 10;
-    let messages = [],
-        fixedResult,
-        fixed = false,
-        passNumber = 0;
-
-    /**
-     * This loop continues until one of the following is true:
-     *
-     * 1. No more fixes have been applied.
-     * 2. Ten passes have been made.
-     *
-     * That means anytime a fix is successfully applied, there will be another pass.
-     * Essentially, guaranteeing a minimum of two passes.
-     */
-    do {
-        passNumber++;
-
-        debug(`Linting code for ${options.filename} (pass ${passNumber})`);
-        messages = linter.verify(text, config, options);
-
-        debug(`Generating fixed text for ${options.filename} (pass ${passNumber})`);
-        fixedResult = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages);
-
-        // stop if there are any syntax errors.
-        // 'fixedResult.output' is a empty string.
-        if (messages.length === 1 && messages[0].fatal) {
-            break;
-        }
-
-        // keep track if any fixes were ever applied - important for return value
-        fixed = fixed || fixedResult.fixed;
-
-        // update to use the fixed output instead of the original text
-        text = fixedResult.output;
-
-    } while (
-        fixedResult.fixed &&
-        passNumber < MAX_PASSES
-    );
-
-
-    /*
-     * If the last result had fixes, we need to lint again to be sure we have
-     * the most up-to-date information.
-     */
-    if (fixedResult.fixed) {
-        fixedResult.messages = linter.verify(text, config, options);
-    }
-
-
-    // ensure the last result properly reflects if fixes were done
-    fixedResult.fixed = fixed;
-    fixedResult.output = text;
-
-    return fixedResult;
-
-}
-
 /**
  * Processes an source code using ESLint.
  * @param {string} text The source code to check.
@@ -269,10 +193,10 @@ function processText(text, configHelper, filename, fix, allowInlineConfig, linte
     } else {
 
         if (fix) {
-            fixedResult = multipassFix(text, config, {
+            fixedResult = linter.verifyAndFix(text, config, {
                 filename,
                 allowInlineConfig
-            }, linter);
+            });
             messages = fixedResult.messages;
         } else {
             messages = linter.verify(text, config, {
diff --git a/lib/ignored-paths.js b/lib/ignored-paths.js
index d2d89bd98..2923ee1a6 100644
--- a/lib/ignored-paths.js
+++ b/lib/ignored-paths.js
@@ -16,7 +16,6 @@ const fs = require("fs"),
 
 const debug = require("debug")("eslint:ignored-paths");
 
-
 //------------------------------------------------------------------------------
 // Constants
 //------------------------------------------------------------------------------
@@ -41,20 +40,38 @@ const DEFAULT_OPTIONS = {
 // Helpers
 //------------------------------------------------------------------------------
 
-
 /**
- * Find an ignore file in the current directory.
+ * Find a file in the current directory.
  * @param {string} cwd Current working directory
+ * @param {string} name File name
  * @returns {string} Path of ignore file or an empty string.
  */
-function findIgnoreFile(cwd) {
+function findFile(cwd, name) {
     cwd = cwd || DEFAULT_OPTIONS.cwd;
 
-    const ignoreFilePath = path.resolve(cwd, ESLINT_IGNORE_FILENAME);
+    const ignoreFilePath = path.resolve(cwd, name);
 
     return fs.existsSync(ignoreFilePath) && fs.statSync(ignoreFilePath).isFile() ? ignoreFilePath : "";
 }
 
+/**
+ * Find an ignore file in the current directory.
+ * @param {string} cwd Current working directory
+ * @returns {string} Path of ignore file or an empty string.
+ */
+function findIgnoreFile(cwd) {
+    return findFile(cwd, ESLINT_IGNORE_FILENAME);
+}
+
+/**
+ * Find an package.json file in the current directory.
+ * @param {string} cwd Current working directory
+ * @returns {string} Path of package.json file or an empty string.
+ */
+function findPackageJSONFile(cwd) {
+    return findFile(cwd, "package.json");
+}
+
 /**
  * Merge options with defaults
  * @param {Object} options Options to merge with DEFAULT_OPTIONS constant
@@ -146,6 +163,37 @@ class IgnoredPaths {
                 this.baseDir = path.dirname(path.resolve(options.cwd, ignorePath));
                 this.addIgnoreFile(this.ig.custom, ignorePath);
                 this.addIgnoreFile(this.ig.default, ignorePath);
+            } else {
+                try {
+
+                    // if the ignoreFile does not exist, check package.json for eslintIgnore
+                    const packageJSONPath = findPackageJSONFile(options.cwd);
+
+                    if (packageJSONPath) {
+                        let packageJSONOptions;
+
+                        try {
+                            packageJSONOptions = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"));
+                        } catch (e) {
+                            debug("Could not read package.json file to check eslintIgnore property");
+                            throw e;
+                        }
+
+                        if (packageJSONOptions.eslintIgnore) {
+                            if (Array.isArray(packageJSONOptions.eslintIgnore)) {
+                                packageJSONOptions.eslintIgnore.forEach(pattern => {
+                                    addPattern(this.ig.custom, pattern);
+                                    addPattern(this.ig.default, pattern);
+                                });
+                            } else {
+                                throw new Error("Package.json eslintIgnore property requires an array of paths");
+                            }
+                        }
+                    }
+                } catch (e) {
+                    debug("Could not find package.json to check eslintIgnore property");
+                    throw e;
+                }
             }
 
             if (options.ignorePattern) {
diff --git a/lib/linter.js b/lib/linter.js
index 25af05223..0b0d06a86 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -27,9 +27,11 @@ const assert = require("assert"),
     Rules = require("./rules"),
     timing = require("./timing"),
     astUtils = require("./ast-utils"),
+    pkg = require("../package.json"),
+    SourceCodeFixer = require("./util/source-code-fixer");
 
-    pkg = require("../package.json");
-
+const debug = require("debug")("eslint:linter");
+const MAX_AUTOFIX_PASSES = 10;
 
 //------------------------------------------------------------------------------
 // Typedefs
@@ -1185,6 +1187,74 @@ class Linter extends EventEmitter {
     getDeclaredVariables(node) {
         return (this.scopeManager && this.scopeManager.getDeclaredVariables(node)) || [];
     }
+
+    /**
+     * Performs multiple autofix passes over the text until as many fixes as possible
+     * have been applied.
+     * @param {string} text The source text to apply fixes to.
+     * @param {Object} config The ESLint config object to use.
+     * @param {Object} options The ESLint options object to use.
+     * @param {string} options.filename The filename from which the text was read.
+     * @param {boolean} options.allowInlineConfig Flag indicating if inline comments
+     *      should be allowed.
+     * @returns {Object} The result of the fix operation as returned from the
+     *      SourceCodeFixer.
+     */
+    verifyAndFix(text, config, options) {
+        let messages = [],
+            fixedResult,
+            fixed = false,
+            passNumber = 0;
+
+        /**
+         * This loop continues until one of the following is true:
+         *
+         * 1. No more fixes have been applied.
+         * 2. Ten passes have been made.
+         *
+         * That means anytime a fix is successfully applied, there will be another pass.
+         * Essentially, guaranteeing a minimum of two passes.
+         */
+        do {
+            passNumber++;
+
+            debug(`Linting code for ${options.filename} (pass ${passNumber})`);
+            messages = this.verify(text, config, options);
+
+            debug(`Generating fixed text for ${options.filename} (pass ${passNumber})`);
+            fixedResult = SourceCodeFixer.applyFixes(this.getSourceCode(), messages);
+
+            // stop if there are any syntax errors.
+            // 'fixedResult.output' is a empty string.
+            if (messages.length === 1 && messages[0].fatal) {
+                break;
+            }
+
+            // keep track if any fixes were ever applied - important for return value
+            fixed = fixed || fixedResult.fixed;
+
+            // update to use the fixed output instead of the original text
+            text = fixedResult.output;
+
+        } while (
+            fixedResult.fixed &&
+            passNumber < MAX_AUTOFIX_PASSES
+        );
+
+        /*
+         * If the last result had fixes, we need to lint again to be sure we have
+         * the most up-to-date information.
+         */
+        if (fixedResult.fixed) {
+            fixedResult.messages = this.verify(text, config, options);
+        }
+
+        // ensure the last result properly reflects if fixes were done
+        fixedResult.fixed = fixed;
+        fixedResult.output = text;
+
+        return fixedResult;
+    }
 }
 
 // methods that exist on SourceCode object
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index 82268975d..5a8feee8f 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -816,7 +816,6 @@ module.exports = {
 
             offsets.ignoreToken(operator);
             offsets.ignoreToken(tokensAfterOperator[0]);
-            offsets.setDesiredOffset(tokensAfterOperator[0], sourceCode.getFirstToken(node), 1);
             offsets.setDesiredOffsets(tokensAfterOperator, tokensAfterOperator[0], 1);
         }
 
@@ -882,6 +881,13 @@ module.exports = {
 
                 // We only want to handle parens around expressions, so exclude parentheses that are in function parameters and function call arguments.
                 if (!parameterParens.has(leftParen) && !parameterParens.has(rightParen)) {
+                    const parenthesizedTokens = new Set(sourceCode.getTokensBetween(leftParen, rightParen));
+
+                    parenthesizedTokens.forEach(token => {
+                        if (!parenthesizedTokens.has(offsets.getFirstDependency(token))) {
+                            offsets.setDesiredOffset(token, leftParen, 1);
+                        }
+                    });
                     offsets.setDesiredOffset(sourceCode.getTokenAfter(leftParen), leftParen, 1);
                 }
 
@@ -1114,28 +1120,29 @@ module.exports = {
                 const tokenBeforeObject = sourceCode.getTokenBefore(node.object, token => astUtils.isNotOpeningParenToken(token) || parameterParens.has(token));
                 const firstObjectToken = tokenBeforeObject ? sourceCode.getTokenAfter(tokenBeforeObject) : sourceCode.ast.tokens[0];
                 const lastObjectToken = sourceCode.getTokenBefore(firstNonObjectToken);
+                const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
 
                 if (node.computed) {
 
                     // For computed MemberExpressions, match the closing bracket with the opening bracket.
                     offsets.matchIndentOf(firstNonObjectToken, sourceCode.getLastToken(node));
+                    offsets.setDesiredOffsets(getTokensAndComments(node.property), firstNonObjectToken, 1);
                 }
 
-                if (typeof options.MemberExpression === "number") {
-                    const firstPropertyToken = node.computed ? firstNonObjectToken : secondNonObjectToken;
+                /*
+                 * If the object ends on the same line that the property starts, match against the last token
+                 * of the object, to ensure that the MemberExpression is not indented.
+                 *
+                 * Otherwise, match against the first token of the object, e.g.
+                 * foo
+                 *   .bar
+                 *   .baz // <-- offset by 1 from `foo`
+                 */
+                const offsetBase = lastObjectToken.loc.end.line === firstPropertyToken.loc.start.line
+                    ? lastObjectToken
+                    : firstObjectToken;
 
-                    /*
-                     * If the object ends on the same line that the property starts, match against the last token
-                     * of the object, to ensure that the MemberExpression is not indented.
-                     *
-                     * Otherwise, match against the first token of the object, e.g.
-                     * foo
-                     *   .bar
-                     *   .baz // <-- offset by 1 from `foo`
-                     */
-                    const offsetBase = lastObjectToken.loc.end.line === firstPropertyToken.loc.start.line
-                        ? lastObjectToken
-                        : firstObjectToken;
+                if (typeof options.MemberExpression === "number") {
 
                     // Match the dot (for non-computed properties) or the opening bracket (for computed properties) against the object.
                     offsets.setDesiredOffset(firstNonObjectToken, offsetBase, options.MemberExpression);
@@ -1150,6 +1157,9 @@ module.exports = {
                     // If the MemberExpression option is off, ignore the dot and the first token of the property.
                     offsets.ignoreToken(firstNonObjectToken);
                     offsets.ignoreToken(secondNonObjectToken);
+
+                    // To ignore the property indentation, ensure that the property tokens depend on the ignored tokens.
+                    offsets.matchIndentOf(offsetBase, firstNonObjectToken);
                     offsets.matchIndentOf(firstNonObjectToken, secondNonObjectToken);
                 }
             },


Test patch:
diff --git a/tests/fixtures/ignored-paths/bad-package-json-ignore/package.json b/tests/fixtures/ignored-paths/bad-package-json-ignore/package.json
new file mode 100644
index 000000000..37174a849
--- /dev/null
+++ b/tests/fixtures/ignored-paths/bad-package-json-ignore/package.json
@@ -0,0 +1,11 @@
+{
+    "name": "mypackage",
+    "version": "0.0.1",
+    "eslintConfig": {
+        "env": {
+            "browser": true,
+            "node": true
+        }
+    },
+    "eslintIgnore": "hello.js"
+}
diff --git a/tests/fixtures/ignored-paths/package-json-ignore/package.json b/tests/fixtures/ignored-paths/package-json-ignore/package.json
new file mode 100644
index 000000000..05436bdb5
--- /dev/null
+++ b/tests/fixtures/ignored-paths/package-json-ignore/package.json
@@ -0,0 +1,11 @@
+{
+    "name": "mypackage",
+    "version": "0.0.1",
+    "eslintConfig": {
+        "env": {
+            "browser": true,
+            "node": true
+        }
+    },
+    "eslintIgnore": ["hello.js", "world.js"]
+}
diff --git a/tests/fixtures/ignored-paths/package.json b/tests/fixtures/ignored-paths/package.json
new file mode 100644
index 000000000..05436bdb5
--- /dev/null
+++ b/tests/fixtures/ignored-paths/package.json
@@ -0,0 +1,11 @@
+{
+    "name": "mypackage",
+    "version": "0.0.1",
+    "eslintConfig": {
+        "env": {
+            "browser": true,
+            "node": true
+        }
+    },
+    "eslintIgnore": ["hello.js", "world.js"]
+}
diff --git a/tests/lib/ignored-paths.js b/tests/lib/ignored-paths.js
index 1631d37f4..1ef1f29cd 100644
--- a/tests/lib/ignored-paths.js
+++ b/tests/lib/ignored-paths.js
@@ -165,7 +165,28 @@ describe("IgnoredPaths", () => {
             );
         });
 
+        it("should use package.json's eslintIgnore files if no specified .eslintignore file", () => {
+            const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath("package-json-ignore") });
 
+            assert.isTrue(ignoredPaths.contains("hello.js"));
+            assert.isTrue(ignoredPaths.contains("world.js"));
+        });
+
+        it("should not use package.json's eslintIgnore files if specified .eslintignore file", () => {
+            const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() });
+
+            assert.isFalse(ignoredPaths.contains("hello.js"));
+            assert.isFalse(ignoredPaths.contains("world.js"));
+            assert.isTrue(ignoredPaths.contains("sampleignorepattern"));
+        });
+
+        it("should error if package.json's eslintIgnore is not an array of file paths", () => {
+            assert.throws(() => {
+                const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath("bad-package-json-ignore") });
+
+                assert.ok(ignoredPaths);
+            }, "Package.json eslintIgnore property requires an array of paths");
+        });
     });
 
     describe("caching file reads", () => {
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index ed5fb2238..9c47fafe5 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -70,4 +70,18 @@ describe("Linter", () => {
             });
         });
     });
+
+    describe("verifyAndFix", () => {
+        it("Fixes the code", () => {
+            const linter = new Linter();
+            const messages = linter.verifyAndFix("var a", {
+                rules: {
+                    semi: 2
+                }
+            }, { filename: "test.js" });
+
+            assert.equal(messages.output, "var a;", "Fixes were applied correctly");
+            assert.isTrue(messages.fixed);
+        });
+    });
 });
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index 335e8882f..fd2619ef9 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -2563,6 +2563,40 @@ ruleTester.run("indent", rule, {
             `,
             options: [4]
         },
+        {
+            code: unIndent`
+                [
+                ] || [
+                ]
+            `
+        },
+        {
+            code: unIndent`
+                (
+                    [
+                    ] || [
+                    ]
+                )
+            `
+        },
+        {
+            code: unIndent`
+                1
+                + (
+                    1
+                )
+            `
+        },
+        {
+            code: unIndent`
+                (
+                    foo && (
+                        bar ||
+                        baz
+                    )
+                )
+            `
+        },
         {
             code: unIndent`
                 var foo =
@@ -2983,6 +3017,41 @@ ruleTester.run("indent", rule, {
             `,
             options: [4, { flatTernaryExpressions: true }]
         },
+        {
+            code: unIndent`
+                function wrap() {
+                    return (
+                        foo ? bar :
+                        baz ? qux :
+                        foobar ? boop :
+                        /*else*/ beep
+                    )
+                }
+            `,
+            options: [4, { flatTernaryExpressions: true }]
+        },
+        {
+            code: unIndent`
+                function wrap() {
+                    return foo
+                        ? bar
+                        : baz
+                }
+            `,
+            options: [4, { flatTernaryExpressions: true }]
+        },
+        {
+            code: unIndent`
+                function wrap() {
+                    return (
+                        foo
+                            ? bar
+                            : baz
+                    )
+                }
+            `,
+            options: [4, { flatTernaryExpressions: true }]
+        },
         {
             code: unIndent`
                 foo(
@@ -3376,6 +3445,23 @@ ruleTester.run("indent", rule, {
             `,
             options: [4, { MemberExpression: "off" }]
         },
+        {
+            code: unIndent`
+                  foo = bar(
+                  ).baz(
+                  )
+            `,
+            options: [4, { MemberExpression: "off" }]
+        },
+        {
+            code: unIndent`
+                foo[
+                    bar ? baz :
+                    qux
+                ]
+            `,
+            options: [4, { flatTernaryExpressions: true }]
+        },
         {
             code: unIndent`
                   foo
@@ -7036,6 +7122,34 @@ ruleTester.run("indent", rule, {
             options: [4],
             errors: expectedErrors([2, 4, 8, "Identifier"])
         },
+        {
+            code: unIndent`
+                [
+                ] || [
+                    ]
+            `,
+            output: unIndent`
+                [
+                ] || [
+                ]
+            `,
+            errors: expectedErrors([3, 0, 4, "Punctuator"])
+        },
+        {
+            code: unIndent`
+                1
+                + (
+                        1
+                    )
+            `,
+            output: unIndent`
+                1
+                + (
+                    1
+                )
+            `,
+            errors: expectedErrors([[3, 4, 8, "Numeric"], [4, 0, 4, "Punctuator"]])
+        },
 
         // Template curlies
         {