mirror of
https://github.com/nix-community/nixvim.git
synced 2026-08-24 10:14:03 -05:00
plugins/lint: add auto-install support
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (builtins)
|
||||
attrValues
|
||||
concatMap
|
||||
isString
|
||||
tryEval
|
||||
;
|
||||
inherit (import ./packages.nix { inherit pkgs lib; }) linter-packages states;
|
||||
|
||||
evalOr =
|
||||
fallback: value:
|
||||
let
|
||||
result = tryEval value;
|
||||
in
|
||||
if result.success then result.value else fallback;
|
||||
|
||||
getTopLevelPackageByName = name: evalOr states.broken (pkgs.${name} or null);
|
||||
in
|
||||
{
|
||||
collectLinters = lintersByFt: lib.unique (lib.flatten (attrValues lintersByFt));
|
||||
|
||||
getPackageOrStateByName =
|
||||
{ definedLinters, overrides }:
|
||||
name:
|
||||
let
|
||||
maybePackage =
|
||||
if builtins.hasAttr name overrides then
|
||||
overrides.${name}
|
||||
else
|
||||
lib.findFirst (value: value != null) states.unknown (
|
||||
[
|
||||
(linter-packages.${name} or null)
|
||||
(getTopLevelPackageByName name)
|
||||
]
|
||||
++ lib.optional (builtins.elem name definedLinters) states.userDefined
|
||||
);
|
||||
in
|
||||
if isString maybePackage then
|
||||
{
|
||||
wrong = {
|
||||
inherit name;
|
||||
reason = maybePackage;
|
||||
};
|
||||
}
|
||||
else
|
||||
{ right = maybePackage; };
|
||||
|
||||
mkWarnsFromStates =
|
||||
opts: list:
|
||||
let
|
||||
mkWarn =
|
||||
{ name, reason }:
|
||||
lib.nixvim.mkWarnings "lint" [
|
||||
{
|
||||
when = true;
|
||||
message = ''
|
||||
You have enabled the '${name}' linter, but nixvim cannot automatically install its package because ${reason}.
|
||||
Set '${opts.autoInstall.overrides}.${name}' to the appropriate package to install it automatically,
|
||||
or set it to `null` to silence this warning. You can also disable all warnings related to packages
|
||||
not installed by `autoInstall` with '${opts.autoInstall.enableWarnings}'.
|
||||
'';
|
||||
}
|
||||
];
|
||||
in
|
||||
concatMap mkWarn list;
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) mkOption types;
|
||||
inherit (lib.nixvim) defaultNullOpts toLuaObject;
|
||||
@@ -95,7 +99,7 @@ let
|
||||
freeformType = with types; attrsOf anything;
|
||||
|
||||
options = builtins.mapAttrs (
|
||||
optionName:
|
||||
_:
|
||||
(
|
||||
{
|
||||
mandatory ? false,
|
||||
@@ -136,6 +140,37 @@ lib.nixvim.plugins.mkNeovimPlugin {
|
||||
maintainers = [ lib.maintainers.HeitorAugustoLN ];
|
||||
|
||||
extraOptions = {
|
||||
autoInstall = {
|
||||
enable = lib.mkEnableOption ''
|
||||
automatic installation of linters listed in `lintersByFt`
|
||||
'';
|
||||
|
||||
enableWarnings = lib.mkOption {
|
||||
default = true;
|
||||
example = false;
|
||||
description = "Whether to enable warnings.";
|
||||
type = lib.types.bool;
|
||||
};
|
||||
|
||||
overrides = lib.mkOption {
|
||||
type = with types; attrsOf (nullOr package);
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"jsonlint" = null;
|
||||
"my-linter" = pkgs.writeShellApplication {
|
||||
name = "my-linter";
|
||||
text = "exec my-linter \"$@\"";
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Attribute set of linter names to nix packages.
|
||||
You can set a linter to `null` to disable auto-installing its package.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
autoCmd =
|
||||
let
|
||||
defaultEvent = "BufWritePost";
|
||||
@@ -212,40 +247,66 @@ lib.nixvim.plugins.mkNeovimPlugin {
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = cfg: {
|
||||
autoCmd = lib.optionals (cfg.autoCmd != null) [ cfg.autoCmd ];
|
||||
plugins.lint.luaConfig.content = ''
|
||||
local __lint = require('lint')
|
||||
''
|
||||
+ (lib.optionalString (cfg.lintersByFt != null) ''
|
||||
__lint.linters_by_ft = ${toLuaObject cfg.lintersByFt}
|
||||
'')
|
||||
+ (lib.optionalString (cfg.customLinters != null) (
|
||||
lib.concatLines (
|
||||
lib.mapAttrsToList (
|
||||
customLinter: linterConfig:
|
||||
let
|
||||
linterConfig' =
|
||||
if builtins.isString linterConfig then lib.nixvim.mkRaw linterConfig else linterConfig;
|
||||
in
|
||||
''__lint.linters["${customLinter}"] = ${toLuaObject linterConfig'}''
|
||||
) cfg.customLinters
|
||||
)
|
||||
))
|
||||
+ (lib.optionalString (cfg.linters != null) (
|
||||
lib.concatLines (
|
||||
lib.flatten (
|
||||
extraConfig =
|
||||
cfg: opts:
|
||||
let
|
||||
inherit (cfg.autoInstall) enable enableWarnings;
|
||||
inherit (import ./auto-install.nix { inherit pkgs lib; })
|
||||
collectLinters
|
||||
getPackageOrStateByName
|
||||
mkWarnsFromStates
|
||||
;
|
||||
configuredLinters = if cfg.linters == null then { } else cfg.linters;
|
||||
customLinters = if cfg.customLinters == null then { } else cfg.customLinters;
|
||||
definedLinters = builtins.attrNames configuredLinters ++ builtins.attrNames customLinters;
|
||||
getPackageOrStateByNameWith = getPackageOrStateByName {
|
||||
inherit definedLinters;
|
||||
inherit (cfg.autoInstall) overrides;
|
||||
};
|
||||
selectedLinters = lib.optionals (cfg.lintersByFt != null) (collectLinters cfg.lintersByFt);
|
||||
packagesAndStates = lib.foldAttrs (item: acc: [ item ] ++ acc) [ ] (
|
||||
map getPackageOrStateByNameWith selectedLinters
|
||||
);
|
||||
in
|
||||
{
|
||||
autoCmd = lib.optionals (cfg.autoCmd != null) [ cfg.autoCmd ];
|
||||
warnings = lib.mkIf (enable && enableWarnings) (
|
||||
mkWarnsFromStates opts (packagesAndStates.wrong or [ ])
|
||||
);
|
||||
extraPackages = lib.mkIf enable (packagesAndStates.right or [ ]);
|
||||
|
||||
plugins.lint.luaConfig.content = ''
|
||||
local __lint = require('lint')
|
||||
''
|
||||
+ (lib.optionalString (cfg.lintersByFt != null) ''
|
||||
__lint.linters_by_ft = ${toLuaObject cfg.lintersByFt}
|
||||
'')
|
||||
+ (lib.optionalString (cfg.customLinters != null) (
|
||||
lib.concatLines (
|
||||
lib.mapAttrsToList (
|
||||
linter: linterConfig:
|
||||
lib.mapAttrsToList (
|
||||
propName: propValue:
|
||||
lib.optionalString (
|
||||
propValue != null
|
||||
) ''__lint.linters["${linter}"].${propName} = ${toLuaObject propValue}''
|
||||
) linterConfig
|
||||
) cfg.linters
|
||||
customLinter: linterConfig:
|
||||
let
|
||||
linterConfig' =
|
||||
if builtins.isString linterConfig then lib.nixvim.mkRaw linterConfig else linterConfig;
|
||||
in
|
||||
''__lint.linters["${customLinter}"] = ${toLuaObject linterConfig'}''
|
||||
) cfg.customLinters
|
||||
)
|
||||
)
|
||||
));
|
||||
};
|
||||
))
|
||||
+ (lib.optionalString (cfg.linters != null) (
|
||||
lib.concatLines (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
linter: linterConfig:
|
||||
lib.mapAttrsToList (
|
||||
propName: propValue:
|
||||
lib.optionalString (
|
||||
propValue != null
|
||||
) ''__lint.linters["${linter}"].${propName} = ${toLuaObject propValue}''
|
||||
) linterConfig
|
||||
) cfg.linters
|
||||
)
|
||||
)
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
}:
|
||||
let
|
||||
inherit (builtins)
|
||||
tryEval
|
||||
;
|
||||
inherit (pkgs) stdenv;
|
||||
|
||||
toPackage = loc: lib.getAttrFromPath (lib.toList loc) pkgs;
|
||||
|
||||
states = {
|
||||
broken = "it is currently broken in nixpkgs";
|
||||
darwinOnly = p: if stdenv.hostPlatform.isDarwin then p else "it is only available on Darwin";
|
||||
linuxOnly = p: if stdenv.hostPlatform.isLinux then p else "it is only available on Linux";
|
||||
unpackaged = "it is not packaged in nixpkgs";
|
||||
unknown = "nixvim does not know which package provides it";
|
||||
userDefined = "it is a user-defined linter";
|
||||
};
|
||||
|
||||
evalOr =
|
||||
fallback: value:
|
||||
let
|
||||
result = tryEval value;
|
||||
in
|
||||
if result.success then result.value else fallback;
|
||||
|
||||
getTopLevelPackageByName = name: evalOr states.broken (pkgs.${name} or null);
|
||||
|
||||
resolvePackageState = name: lib.defaultTo states.unpackaged (getTopLevelPackageByName name);
|
||||
|
||||
resolvePackage =
|
||||
name: loc:
|
||||
let
|
||||
default = resolvePackageState name;
|
||||
in
|
||||
if loc == null then default else evalOr default (toPackage loc);
|
||||
|
||||
resolvePackages = packaged: lib.mapAttrs resolvePackage packaged;
|
||||
|
||||
resolveNames =
|
||||
names: builtins.listToAttrs (map (name: lib.nameValuePair name (resolvePackageState name)) names);
|
||||
|
||||
efmlsPackages = import ../efmls-configs/packages.nix lib;
|
||||
noneLsPackages = import ../none-ls/packages.nix lib;
|
||||
in
|
||||
{
|
||||
inherit states;
|
||||
|
||||
linter-packages =
|
||||
# These upstream package maps can contain stale "unpackaged" markers and broken aliases.
|
||||
# Resolve them against the current nixpkgs first so auto-install warns instead of exploding.
|
||||
resolveNames efmlsPackages.unpackaged
|
||||
// resolvePackages efmlsPackages.packaged
|
||||
// resolvePackages noneLsPackages.packaged
|
||||
// {
|
||||
biomejs = pkgs.biome;
|
||||
buf_lint = pkgs.buf;
|
||||
clazy = states.linuxOnly pkgs.clazy;
|
||||
clangtidy = pkgs."clang-tools";
|
||||
cmakelint = pkgs."cmake-format";
|
||||
dmypy = pkgs.mypy;
|
||||
inherit (pkgs.luaPackages) fennel;
|
||||
ghdl = states.linuxOnly pkgs.ghdl;
|
||||
golangcilint = pkgs."golangci-lint";
|
||||
json_tool = pkgs.python3;
|
||||
ls_lint = pkgs."ls-lint";
|
||||
mago_analyze = pkgs.mago;
|
||||
mago_lint = pkgs.mago;
|
||||
opa_check = pkgs."open-policy-agent";
|
||||
inherit (pkgs.phpPackages) phpinsights;
|
||||
inherit (pkgs) phpstan;
|
||||
pony = pkgs.ponyc;
|
||||
inherit (pkgs.python3Packages) pycodestyle;
|
||||
inherit (pkgs.python3Packages) pydocstyle;
|
||||
rstlint = pkgs.python3Packages."restructuredtext-lint";
|
||||
systemd-analyze = states.linuxOnly pkgs.systemd;
|
||||
tofu = pkgs.opentofu;
|
||||
vacuum = states.linuxOnly pkgs.vacuum;
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,207 @@
|
||||
{ lib, pkgs, ... }:
|
||||
{
|
||||
empty = {
|
||||
plugins.lint.enable = true;
|
||||
};
|
||||
|
||||
all-linters =
|
||||
let
|
||||
inherit (import ../../../../../plugins/by-name/lint/auto-install.nix { inherit lib pkgs; })
|
||||
getPackageOrStateByName
|
||||
;
|
||||
resolvedLinters = map (name: {
|
||||
inherit name;
|
||||
result = getPackageOrStateByName {
|
||||
definedLinters = [ ];
|
||||
overrides = { };
|
||||
} name;
|
||||
}) (lib.importJSON ../../../../../generated/lint-linters.json);
|
||||
installableLinters = map (x: x.name) (
|
||||
builtins.filter (
|
||||
x:
|
||||
x.result ? right
|
||||
&& lib.meta.availableOn pkgs.stdenv.hostPlatform x.result.right
|
||||
&& !(x.result.right.meta.broken or false)
|
||||
) resolvedLinters
|
||||
);
|
||||
badPackageLinters = builtins.filter (
|
||||
x:
|
||||
x.result ? right
|
||||
&& (
|
||||
!lib.meta.availableOn pkgs.stdenv.hostPlatform x.result.right
|
||||
|| (x.result.right.meta.broken or false)
|
||||
)
|
||||
) resolvedLinters;
|
||||
in
|
||||
{
|
||||
plugins.lint = {
|
||||
enable = true;
|
||||
autoInstall = {
|
||||
enable = true;
|
||||
enableWarnings = false;
|
||||
};
|
||||
lintersByFt."*" = installableLinters;
|
||||
};
|
||||
|
||||
test = {
|
||||
buildNixvim = false;
|
||||
warnings = expect: [ (expect "count" 0) ];
|
||||
};
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = badPackageLinters == [ ];
|
||||
message =
|
||||
"The following documented nvim-lint linters resolve to broken or unavailable packages and need an explicit state in plugins/by-name/lint/packages.nix: "
|
||||
+ lib.concatMapStringsSep ", " (
|
||||
x: "${x.name} -> ${x.result.right.pname or x.result.right.name or "drv"}"
|
||||
) badPackageLinters;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
auto-install =
|
||||
{ config, ... }:
|
||||
{
|
||||
plugins.lint = {
|
||||
enable = true;
|
||||
autoInstall.enable = true;
|
||||
lintersByFt = {
|
||||
json = [ "jsonlint" ];
|
||||
terraform = [
|
||||
"tflint"
|
||||
"tofu"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
test = {
|
||||
buildNixvim = false;
|
||||
warnings = expect: [
|
||||
(expect "count" 1)
|
||||
(expect "any" "jsonlint")
|
||||
(expect "any" "not packaged in nixpkgs")
|
||||
];
|
||||
};
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = lib.elem pkgs.tflint config.extraPackages;
|
||||
message = "Expected `tflint` to be in `extraPackages`.";
|
||||
}
|
||||
{
|
||||
assertion = lib.elem pkgs.opentofu config.extraPackages;
|
||||
message = "Expected `opentofu` to be in `extraPackages` for the `tofu` linter.";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
linux-only-auto-install =
|
||||
{ config, ... }:
|
||||
let
|
||||
inherit (pkgs.stdenv) isLinux;
|
||||
in
|
||||
{
|
||||
plugins.lint = {
|
||||
enable = true;
|
||||
autoInstall.enable = true;
|
||||
lintersByFt.systemd = [ "systemd-analyze" ];
|
||||
};
|
||||
|
||||
test = {
|
||||
buildNixvim = false;
|
||||
warnings =
|
||||
expect:
|
||||
if isLinux then
|
||||
[ (expect "count" 0) ]
|
||||
else
|
||||
[
|
||||
(expect "count" 1)
|
||||
(expect "any" "only available on Linux")
|
||||
];
|
||||
};
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = isLinux == lib.elem pkgs.systemd config.extraPackages;
|
||||
message =
|
||||
if isLinux then
|
||||
"Expected `systemd` to be in `extraPackages` on Linux."
|
||||
else
|
||||
"Expected `systemd` not to be in `extraPackages` on non-Linux systems.";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
auto-install-renamed-package =
|
||||
{ config, ... }:
|
||||
{
|
||||
plugins.lint = {
|
||||
enable = true;
|
||||
autoInstall.enable = true;
|
||||
lintersByFt.fennel = [ "fennel" ];
|
||||
};
|
||||
|
||||
test = {
|
||||
buildNixvim = false;
|
||||
warnings = expect: [ (expect "count" 0) ];
|
||||
};
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = lib.elem pkgs.luaPackages.fennel config.extraPackages;
|
||||
message = "Expected `luaPackages.fennel` to be in `extraPackages` for the `fennel` linter.";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
auto-install-broken-alias =
|
||||
{ config, ... }:
|
||||
{
|
||||
plugins.lint = {
|
||||
enable = true;
|
||||
autoInstall.enable = true;
|
||||
lintersByFt.php = [ "phpstan" ];
|
||||
};
|
||||
|
||||
test = {
|
||||
buildNixvim = false;
|
||||
warnings = expect: [ (expect "count" 0) ];
|
||||
};
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = lib.elem pkgs.phpstan config.extraPackages;
|
||||
message = "Expected `phpstan` to be in `extraPackages` for the `phpstan` linter.";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
auto-install-null-override =
|
||||
{ config, ... }:
|
||||
{
|
||||
plugins.lint = {
|
||||
enable = true;
|
||||
autoInstall = {
|
||||
enable = true;
|
||||
overrides.jsonlint = null;
|
||||
};
|
||||
lintersByFt.json = [ "jsonlint" ];
|
||||
};
|
||||
|
||||
test = {
|
||||
buildNixvim = false;
|
||||
warnings = expect: [ (expect "count" 0) ];
|
||||
};
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.extraPackages == [ ];
|
||||
message = "Expected `null` overrides to skip auto-installing the linter package.";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
example = {
|
||||
plugins.lint = {
|
||||
enable = true;
|
||||
|
||||
Reference in New Issue
Block a user