-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
851 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
--- | ||
pageClass: "rule-details" | ||
sidebarDepth: 0 | ||
title: "node-dependencies/absolute-version" | ||
description: "require or disallow absolute version of dependency." | ||
--- | ||
# node-dependencies/absolute-version | ||
|
||
> require or disallow absolute version of dependency. | ||
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge> | ||
|
||
## :book: Rule Details | ||
|
||
This rule enforces the use of absolute version of dependency. | ||
|
||
```json5 | ||
{ | ||
"devDependencies": { | ||
"semver": "^7.3.5", /* ✗ BAD */ | ||
"typescript": "4.6.2" /* ✓ GOOD */ | ||
} | ||
} | ||
``` | ||
|
||
## :wrench: Options | ||
|
||
### String Option | ||
|
||
```json5 | ||
{ | ||
"node-dependencies/absolute-version": ["error", | ||
"always" // or "never" | ||
] | ||
} | ||
``` | ||
|
||
- `"always"` ... Enforces to use the absolute version. | ||
- `"never"` ... Enforces not to use the absolute version. | ||
|
||
### Object Option | ||
|
||
```json5 | ||
{ | ||
"node-dependencies/absolute-version": ["error", { | ||
"dependencies": "ignore", // , "always" or "never" | ||
"peerDependencies": "ignore", // , "always" or "never" | ||
"optionalDependencies": "ignore", // , "always" or "never" | ||
"devDependencies": "always" // , "never" or "ignore" | ||
}] | ||
} | ||
``` | ||
|
||
- `dependencies` ... Configuration for `dependencies`. | ||
- `peerDependencies` ... Configuration for `dependencies`. | ||
- `optionalDependencies` ... Configuration for `dependencies`. | ||
- `devDependencies` ... Configuration for `dependencies`. | ||
- Value | ||
- `"always"` ... Enforces to use the absolute version. | ||
- `"never"` ... Enforces not to use the absolute version. | ||
- `"ignore"` ... Ignored from the check. | ||
|
||
By default, `always` applies only to `devDependencies`. | ||
|
||
### Override Option for Each Package | ||
|
||
```json5 | ||
{ | ||
"node-dependencies/absolute-version": ["error", { | ||
// "dependencies": "ignore", | ||
// "peerDependencies": "ignore", | ||
// "optionalDependencies": "ignore", | ||
// "devDependencies": "always", | ||
"overridePackages": { | ||
"foo": "always" // Always use an absolute version for the "foo" package. | ||
} | ||
}] | ||
} | ||
``` | ||
|
||
- `overridePackages` ... Configure an object with the package name as the key. | ||
- Property Key ... Specify the package name, or the pattern such as `/^@babel\//`. | ||
- Property Value ... Can use the Object Option or the String Option. | ||
|
||
## :mag: Implementation | ||
|
||
- [Rule source](https://github.com/ota-meshi/eslint-plugin-node-dependencies/blob/main/lib/rules/absolute-version.ts) | ||
- [Test source](https://github.com/ota-meshi/eslint-plugin-node-dependencies/blob/main/tests/lib/rules/absolute-version.ts) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
import { getStaticJSONValue } from "jsonc-eslint-parser" | ||
import type { JSONProperty } from "jsonc-eslint-parser/lib/parser/ast" | ||
import type { Range } from "semver" | ||
import { createRule, defineJsonVisitor } from "../utils" | ||
import { getKeyFromJSONProperty } from "../utils/ast-utils" | ||
import { toRegExp } from "../utils/regexp" | ||
import { getSemverRange, isAnyComparator } from "../utils/semver" | ||
|
||
const PREFERS = ["always" as const, "never" as const, "ignore" as const] | ||
const SCHEMA_FOR_DEPS_PROPERTIES = { | ||
dependencies: { enum: PREFERS }, | ||
peerDependencies: { enum: PREFERS }, | ||
optionalDependencies: { enum: PREFERS }, | ||
devDependencies: { enum: PREFERS }, | ||
} | ||
type Prefer = typeof PREFERS[number] | ||
type FullOption = { | ||
dependencies: Prefer | ||
peerDependencies: Prefer | ||
optionalDependencies: Prefer | ||
devDependencies: Prefer | ||
} | ||
|
||
const DEFAULT: FullOption = { | ||
dependencies: "ignore", | ||
peerDependencies: "ignore", | ||
optionalDependencies: "ignore", | ||
devDependencies: "always", | ||
} | ||
|
||
/** | ||
* Convert from string option to object option | ||
*/ | ||
function stringToOption(option: Prefer): FullOption { | ||
return { | ||
dependencies: option, | ||
peerDependencies: option, | ||
optionalDependencies: option, | ||
devDependencies: option, | ||
} | ||
} | ||
|
||
/** | ||
* Convert from object option to object option | ||
*/ | ||
function objectToOption( | ||
option: Partial<FullOption>, | ||
defaults: FullOption, | ||
): FullOption { | ||
return { | ||
dependencies: option.dependencies || defaults.dependencies, | ||
peerDependencies: option.peerDependencies || defaults.peerDependencies, | ||
optionalDependencies: | ||
option.optionalDependencies || defaults.optionalDependencies, | ||
devDependencies: option.devDependencies || defaults.devDependencies, | ||
} | ||
} | ||
|
||
/** | ||
* Parse option | ||
*/ | ||
function parseOption( | ||
option: | ||
| null | ||
| Prefer | ||
| (Partial<FullOption> & { | ||
overridePackages?: Record<string, Prefer | Partial<FullOption>> | ||
}), | ||
): (packageName: string) => FullOption { | ||
if (!option) { | ||
return () => DEFAULT | ||
} | ||
if (typeof option === "string") { | ||
const objectOption = stringToOption(option) | ||
return () => objectOption | ||
} | ||
const baseOption = objectToOption(option, DEFAULT) | ||
if (!option.overridePackages) { | ||
return () => baseOption | ||
} | ||
const overridePackages = Object.entries(option.overridePackages).map( | ||
([packageName, opt]) => { | ||
const regexp = toRegExp(packageName) | ||
return { | ||
test: (s: string) => regexp.test(s), | ||
...(typeof opt === "string" | ||
? stringToOption(opt) | ||
: objectToOption(opt, baseOption)), | ||
} | ||
}, | ||
) | ||
|
||
return (name) => { | ||
for (const overridePackage of overridePackages) { | ||
if (overridePackage.test(name)) { | ||
return overridePackage | ||
} | ||
} | ||
|
||
return baseOption | ||
} | ||
} | ||
|
||
export default createRule("absolute-version", { | ||
meta: { | ||
docs: { | ||
description: "require or disallow absolute version of dependency.", | ||
category: "Best Practices", | ||
recommended: false, | ||
}, | ||
schema: [ | ||
{ | ||
oneOf: [ | ||
{ enum: PREFERS.filter((p) => p !== "ignore") }, | ||
{ | ||
type: "object", | ||
properties: { | ||
...SCHEMA_FOR_DEPS_PROPERTIES, | ||
overridePackages: { | ||
type: "object", | ||
patternProperties: { | ||
"^(?:\\S+)$": { | ||
oneOf: [ | ||
{ enum: PREFERS }, | ||
{ | ||
type: "object", | ||
properties: | ||
SCHEMA_FOR_DEPS_PROPERTIES, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
}, | ||
minProperties: 1, | ||
additionalProperties: false, | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
], | ||
messages: {}, | ||
type: "suggestion", | ||
}, | ||
create(context) { | ||
const getOption = parseOption(context.options[0]) | ||
|
||
/** Define dependency visitor */ | ||
function defineVisitor( | ||
visitName: | ||
| "dependencies" | ||
| "peerDependencies" | ||
| "optionalDependencies" | ||
| "devDependencies", | ||
) { | ||
return (node: JSONProperty) => { | ||
const ver = getStaticJSONValue(node.value) | ||
if (typeof ver !== "string" || ver == null) { | ||
return | ||
} | ||
const name = String(getKeyFromJSONProperty(node)) | ||
const option = getOption(name)[visitName] | ||
|
||
const semver = getSemverRange(ver) | ||
if (semver == null) { | ||
return | ||
} | ||
if (option === "always") { | ||
if (isAbsoluteVersion(semver)) { | ||
return | ||
} | ||
context.report({ | ||
loc: node.value.loc, | ||
message: "Use the absolute version instead.", | ||
}) | ||
} else if (option === "never") { | ||
if (!isAbsoluteVersion(semver)) { | ||
return | ||
} | ||
context.report({ | ||
loc: node.value.loc, | ||
message: "Do not use the absolute version.", | ||
}) | ||
} | ||
} | ||
} | ||
|
||
return defineJsonVisitor({ | ||
dependencies: defineVisitor("dependencies"), | ||
peerDependencies: defineVisitor("peerDependencies"), | ||
optionalDependencies: defineVisitor("optionalDependencies"), | ||
devDependencies: defineVisitor("devDependencies"), | ||
}) | ||
}, | ||
}) | ||
|
||
/** Checks whether the given semver is absolute version or not */ | ||
function isAbsoluteVersion(semver: Range) { | ||
for (const comparators of semver.set) { | ||
for (const comparator of comparators) { | ||
if (isAnyComparator(comparator)) { | ||
return false | ||
} | ||
if (comparator.operator !== "=" && comparator.operator !== "") { | ||
return false | ||
} | ||
} | ||
} | ||
return true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* Convert to regexp | ||
*/ | ||
export function toRegExp(str: string): { test(s: string): boolean } { | ||
const regexp = parseRegExp(str) | ||
if (regexp) { | ||
return regexp | ||
} | ||
return { test: (s) => s === str } | ||
} | ||
|
||
/** | ||
* Parse regexp string | ||
*/ | ||
function parseRegExp(str: string) { | ||
if (!str.startsWith("/")) { | ||
return null | ||
} | ||
const lastSlashIndex = str.lastIndexOf("/") | ||
if (lastSlashIndex <= 1) { | ||
return null | ||
} | ||
return new RegExp( | ||
str.slice(1, lastSlashIndex), | ||
str.slice(lastSlashIndex + 1), | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.