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"]
    }
  ]
}

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 `PROJECT_CONFIG_TEST` with the value
      files: [".project-config.toml"],
      JMESPathsMatch: [
        [
          "op(op(type(cache), '!=', 'string'), '|', op(type(setenv('PROJECT_CONFIG_TEST', 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('PROJECT_CONFIG_TEST'))", "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('PROJECT_CONFIG_TEST'))", "string"],
        ],
      },
      JMESPathsMatch: [["type(setenv('PROJECT_CONFIG_TEST', null))", "object"]],
    },
  ],
}

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 = [
  ["project.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.22/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: "The line '__pycache__/' must be present in .gitignore file",
      includeLines: ["__pycache__/"],
    },
    {
      files: [".gitignore"],
      hint: "As a directory 'tests/' has been found, the line '.pytest_cache/' must be present in the '.gitignore' file",
      ifFilesExist: ["tests/"],
      includeLines: [".pytest_cache/"],
    },
    {
      files: [".gitignore"],
      hint: "As a directory 'tests/' has been found, the line '.pytest_cache' must not be present as it is implicitly naming a directory",
      ifFilesExist: ["tests/"],
      excludeLines: [".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"],
      preCommitHookExists: [
        "https://github.com/editorconfig-checker/editorconfig-checker.python",
        [
          {
            id: "editorconfig-checker",
            name: "editorconfig-checker",
            alias: "ec",
          },
        ],
      ],
    },
  ],
}

Tip

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