Examples

Basic usage

The file .gitignore must have a line with the content /dist/.

{
  "rules": [
    {
      "files": [".gitignore"],
      "includeLines": ["/dist/"]
    }
  ]
}

project-config self configuration

project-config is defining a valid configuration, forcing the definition of styles as an array for styles and a valid cache value.

{
  rules: [
    {
      files: [".project-config.toml"],
      JMESPathsMatch: [
        // `style` must be defined in the file
        ["contains(keys(@), 'style')", true],
        // `style` must be an array
        ["type(style)", "array"],
        // at least one style configured
        ["op(length(style), '>', `0`)", true],

        // configure cache explicitly
        ["contains(keys(@), 'cache')", true],
        // cache must have a valid value
        [
          "regex_match('^(\\d+ ((seconds?)|(minutes?)|(hours?)|(days?)|(weeks?)))|(never)$', cache)",
          true,
          "set(@, 'cache', '5 minutes')",
        ],
      ],
    },
  ],
}

Files absence

The files readme.md and index.md must not exist.

rules:
  - files:
      not:
        readme.md: Users are more used to seeing README.md file name in uppercase
    hint: Rename 'readme.md' to 'README.md'
  - files:
      not:
        - index.md

Conditionals

If .gitignore includes the line __pycache__/ a pyproject.toml file must be present.

{
  "rules": [
    {
      "ifIncludeLines": {
        ".gitignore": ["__pycache__/"]
      },
      "files": ["pyproject.toml"]
    }
  ]
}

Conditionals files existence

Follows the next rules:
  • If the directory src/ exists, the file pyproject.toml must exists too.

  • If the file pyproject.toml exists, a Python file must be present in the root directory.

{
  rules: [
    {
      files: ["pyproject.toml"],
      ifFilesExist: ["src/"],
    },
    {
      files: ["*.py"],
      ifFilesExist: ["pyproject.toml"],
    },
  ],
}

Compare values between files

The version defined in __version__ inside a Python script must match the metadata defined in pyproject.toml file.

[[rules]]
files = ["pyproject.toml"]
crossJMESPathsMatch = [
  [
    "tool.poetry.version",
    ["script.py", "__version__"],
    "op([0], '==', [1])",
    true,
  ],
]

JMESPath against online sources

Check that the license field of package.json file is defined with a valid OSI approved SPDX license identifier.

{
  rules: [
    {
      files: ["package.json"],
      crossJMESPathsMatch: [
        [
          "license",
          [
            "gh://spdx/license-list-data@v3.17/json/licenses.json",
            "licenses[?isOsiApproved] | [?!isDeprecatedLicenseId].licenseId",
          ],
          "contains([1], [0])",
          true,
        ],
      ],
    },
  ],
}

Assert root directory name

Check that the name of the directory that is the root of the project matches against certain regular expression.

{
  rules: [
    {
      files: [".project-config.toml"],
      hint: "The name of the root directory must match the regex '[a-z0-9-]+$'",
      JMESPathsMatch: [["regex_match('[a-z0-9-]+$', rootdir_name())", true]],
    },
  ],
}

TOML sections order

Check that the section [foo] of a TOML file is placed before the section [bar].

{
  rules: [
    {
      files: ["pyproject.toml"],
      hint: "The section '[foo]' must be defined before the section '[bar]'",
      crossJMESPathsMatch: [
        [
          "null",
          ["pyproject.toml?text", "@"],
          "op(op([1], 'indexOf', '[foo]'), '<', op([1], 'indexOf', '[bar]'))",
          true,
        ],
      ],
    },
  ],
}

Editing a .gitignore file

Enforce the existence of certain lines in a .gitignore file.

{
  rules: [
    {
      files: [".gitignore"], // Enforce the existence of a '.gitignore' file
      hint: "Enforced the line '__pycache__/' to be present in .gitignore file",
      includeLines: ["__pycache__/"],
    },
    {
      files: [".gitignore"],
      hint: "As a directory 'tests/' has been found, enforced the line '.pytest_cache/' to be present in the '.gitignore' file",
      ifFilesExist: ["tests/"],
      includeLines: [".pytest_cache/"],
    },
    {
      files: [".gitignore"],
      hint: "Removed the line '.pytest_cache' from the '.gitignore' file as it is implicitly naming a directory",
      JMESPathsMatch: [
        ["contains(@, '.pytest_cache')", false, "[?@ != '.pytest_cache']"],
      ],
    },
    {
      files: [".gitignore"],
      hint: "Enforce '*.egg-info/' at the end of the .gitignore if is not already present",
      includeLines: [
        [
          "*.egg-info/",
          "op([?!starts_with(@, '*.egg-info')], `+`, ['*.egg-info/'])",
        ],
      ],
    },
    {
      files: [".gitignore"],
      hint: "The line 'dist/' must be included in .gitignore",
      includeLines: [
        [
          "dist/",
          "op([?!contains(['/dist/', 'dist', 'dist/'], @)], '+', ['dist/'])",
        ],
        "__pycache__/",
      ],
    },
  ],
}

Replacing code blocks languages in RST documents

Don’t allow code blocks in RST documentation files:
  • Bash is not a POSIX compliant shell, use Shell lexer.

  • Pygments’ JSON5 lexer is not implemented yet, use Javascript lexer.

{
  rules: [
    {
      files: ["file.rst"],
      excludeContent: [
        [
          ".. code-block::  ",
          "map(&replace(@, 'code-block::  ', 'code-block:: '), @)",
        ],
        [
          ".. code-block:: bash",
          "map(&replace(@, 'code-block:: bash', 'code-block:: sh'), @)",
        ],
        [
          ".. code-block:: json5",
          "map(&replace(@, 'code-block:: json5', 'code-block:: js'), @)",
        ],
      ],
    },
  ],
}

Autofixing .editorconfig

If you run the next example using project-config fix subcommand without creating an .editorconfig file it will be created and populated with the sections defined in the rule.

{
  rules: [
    {
      files: [".editorconfig"],
      JMESPathsMatch: [
        ["type(@)", "object"],
        ['type("")', "object"],
        ['"".root', true],
        ['"*".end_of_line', "lf"],
        ['"*".indent_style', "space"],
        ['"*".charset', "utf-8"],
        ['"*".trim_trailing_whitespace', true],
      ],
    },
  ],
}

Setting hooks in .pre-commit-config.yaml

Set a pre-commit hook for editorconfig-checker inside .pre-commit-config.yaml with manual fixes.

{
  rules: [
    {
      files: [".pre-commit-config.yaml"],
      ifJMESPathsMatch: {
        ".pre-commit-config.yaml": [
          ["type(@)", "object"],
          ["type(repos)", "array"],
        ],
      },
      JMESPathsMatch: [
        // Projects must use editorconfig checker pre-commit hook
        // to follow editorconfig rules. If the repo is not found in the list
        // of repos at this point, it will be added:
        [
          "length(repos[?repo=='https://github.com/editorconfig-checker/editorconfig-checker.python'])",
          1,
          "set(@, 'repos', insert(repos, `-1`, {rev: gh_tags('editorconfig-checker', 'editorconfig-checker.python', True)[0], repo: 'https://github.com/editorconfig-checker/editorconfig-checker.python', hooks: [{id: 'editorconfig-checker', name: 'editorconfig-checker', alias: 'ec'}]}))",
        ],

        //
        // After it we can check that the fields have been successfully added:
        //
        [
          // the 'rev' key must match the X.Y.Z regex pattern
          "regex_match('^\\d+\\.\\d+\\.\\d+$', repos[?repo=='https://github.com/editorconfig-checker/editorconfig-checker.python'] | [0].rev)",
          true,
        ],
        [
          // must contain hooks
          "type(repos[?repo=='https://github.com/editorconfig-checker/editorconfig-checker.python'] | [0].hooks)",
          "array",
        ],

        [
          // the hook has an `id` field with 'editorconfig-checker' value
          "contains(repos[?repo=='https://github.com/editorconfig-checker/editorconfig-checker.python'] | [0].hooks[*].id, 'editorconfig-checker')",
          true,
        ],
        [
          // the hook has a `name` field with 'editorconfig-checker' value
          "repos[?repo=='https://github.com/editorconfig-checker/editorconfig-checker.python'] | [0].hooks[?id=='editorconfig-checker'] | [0].name",
          "editorconfig-checker",
        ],
        [
          // the hook has an `alias` field with 'c' value
          "repos[?repo=='https://github.com/editorconfig-checker/editorconfig-checker.python'] | [0].hooks[?id=='editorconfig-checker'] | [0].alias",
          "ec",
        ],
      ],
    },
  ],
}

Intercommunication between rules

Using getenv() and setenv() functions to pass arbitrary data between rules.

{
  rules: [
    {
      // Setting environment variables
      //
      // If `cache` field is of type string, set an environment
      // variable `PREVIOUS_PROJECT_CONFIG_CACHE` with the value
      files: [".project-config.toml"],
      JMESPathsMatch: [
        [
          "op(op(type(cache), '!=', 'string'), '|', op(type(setenv('PREVIOUS_PROJECT_CONFIG_CACHE', cache)), '==', 'object'))",
          true,
        ],
      ],
    },
    {
      // Getting environment variables
      //
      // If the previous environment variable is setted, change `cache`
      // to `never`.
      files: [".project-config.toml"],
      ifJMESPathsMatch: {
        ".project-config.toml": [
          ["type(getenv('PREVIOUS_PROJECT_CONFIG_CACHE'))", "string"],
        ],
      },
      JMESPathsMatch: [["cache", "never"]],
    },
    {
      // Deleting environment variables
      //
      // If the previous environment variable is setted, delete it.
      files: [".project-config.toml"],
      ifJMESPathsMatch: {
        ".project-config.toml": [
          ["type(getenv('PREVIOUS_PROJECT_CONFIG_CACHE'))", "string"],
        ],
      },
      JMESPathsMatch: [
        ["type(setenv('PREVIOUS_PROJECT_CONFIG_CACHE', null))", "object"],
      ],
    },
  ],
}

Tip

For more complex examples check my own styles at mondeja/project-config-styles.