Skip to content

Commit

Permalink
part: set up overridable options
Browse files Browse the repository at this point in the history
  • Loading branch information
RebeccaStevens committed May 6, 2024
1 parent e582e36 commit a8aaa5c
Show file tree
Hide file tree
Showing 7 changed files with 430 additions and 2 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
"deepmerge-ts": "^5.1.0",
"escape-string-regexp": "^4.0.0",
"is-immutable-type": "^4.0.0",
"ts-api-utils": "^1.3.0"
"ts-api-utils": "^1.3.0",
"ts-declaration-location": "^1.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "7.24.5",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/options/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./ignore";
export * from "./overrides";
141 changes: 141 additions & 0 deletions src/options/overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { type TSESTree } from "@typescript-eslint/utils";
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
import { deepmerge } from "deepmerge-ts";
import typeMatchesSpecifier from "ts-declaration-location";

import { getTypeDataOfNode } from "#eslint-plugin-functional/utils/rule";
import {
type RawTypeSpecifier,
type TypeSpecifier,
typeMatchesPattern,
} from "#eslint-plugin-functional/utils/type-specifier";

/**
* Options that can be overridden.
*/
export type OverridableOptions<CoreOptions> = CoreOptions & {
overrides?: Array<
{
specifiers: TypeSpecifier | TypeSpecifier[];
} & (
| {
options: CoreOptions;
inherit?: boolean;
disable?: false;
}
| {
disable: true;
}
)
>;
};

export type RawOverridableOptions<CoreOptions> = CoreOptions & {
overrides?: Array<{
specifiers?: RawTypeSpecifier | RawTypeSpecifier[];
options?: CoreOptions;
inherit?: boolean;
disable?: boolean;
}>;
};

export function upgradeRawOverridableOptions<CoreOptions>(
raw: RawOverridableOptions<CoreOptions>,
): OverridableOptions<CoreOptions> {
return {
...raw,
overrides:
raw.overrides?.map((override) => ({
...override,
specifiers:
override.specifiers === undefined
? []
: Array.isArray(override.specifiers)
? override.specifiers.map(upgradeRawTypeSpecifier)
: [upgradeRawTypeSpecifier(override.specifiers)],
})) ?? [],
} as OverridableOptions<CoreOptions>;
}

function upgradeRawTypeSpecifier(raw: RawTypeSpecifier): TypeSpecifier {
const { ignoreName, ignorePattern, name, pattern, ...rest } = raw;

const names = name === undefined ? [] : Array.isArray(name) ? name : [name];

const patterns = (
pattern === undefined ? [] : Array.isArray(pattern) ? pattern : [pattern]
).map((p) => new RegExp(p, "u"));

const ignoreNames =
ignoreName === undefined
? []
: Array.isArray(ignoreName)
? ignoreName
: [ignoreName];

const ignorePatterns = (
ignorePattern === undefined
? []
: Array.isArray(ignorePattern)
? ignorePattern
: [ignorePattern]
).map((p) => new RegExp(p, "u"));

const include = [...names, ...patterns];
const exclude = [...ignoreNames, ...ignorePatterns];

return {
...rest,
include,
exclude,
};
}

/**
* Get the core options to use, taking into account overrides.
*/
export function getCoreOptions<
CoreOptions extends object,
Options extends Readonly<OverridableOptions<CoreOptions>>,
>(
node: TSESTree.Node,
context: Readonly<RuleContext<string, unknown[]>>,
options: Readonly<Options>,
): CoreOptions | null {
const program = context.sourceCode.parserServices?.program ?? undefined;
if (program === undefined) {
return options;
}

const [type, typeNode] = getTypeDataOfNode(node, context);
const found = options.overrides?.find((override) =>
(Array.isArray(override.specifiers)
? override.specifiers
: [override.specifiers]
).some(
(specifier) =>
typeMatchesSpecifier(program, specifier, type) &&
(specifier.include === undefined ||
specifier.include.length === 0 ||
typeMatchesPattern(
program,
type,
typeNode,
specifier.include,
specifier.exclude,
)),
),
);

if (found !== undefined) {
if (found.disable === true) {
return null;
}
if (found.inherit !== false) {
return deepmerge(options, found.options) as CoreOptions;
}
return found.options;
}

return options;
}
42 changes: 41 additions & 1 deletion src/utils/rule.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import assert from "node:assert/strict";

import { type TSESTree } from "@typescript-eslint/utils";
import {
type NamedCreateRuleMeta,
Expand All @@ -21,6 +23,8 @@ import { getImmutabilityOverrides } from "#eslint-plugin-functional/settings";
import { __VERSION__ } from "#eslint-plugin-functional/utils/constants";
import { type ESFunction } from "#eslint-plugin-functional/utils/node-types";

import { typeMatchesPattern } from "./type-specifier";

/**
* Any custom rule meta properties.
*/
Expand Down Expand Up @@ -187,10 +191,45 @@ export function getTypeOfNode<Context extends RuleContext<string, BaseOptions>>(
node: TSESTree.Node,
context: Context,
): Type {
assert(ts !== undefined);

const { esTreeNodeToTSNodeMap } = getParserServices(context);

const tsNode = esTreeNodeToTSNodeMap.get(node);
return getTypeOfTSNode(tsNode, context);
const typedNode = ts.isIdentifier(tsNode) ? tsNode.parent : tsNode;
return getTypeOfTSNode(typedNode, context);
}

/**
* Get the type of the the given node.
*/
export function getTypeNodeOfNode<
Context extends RuleContext<string, BaseOptions>,
>(node: TSESTree.Node, context: Context): TypeNode | null {
assert(ts !== undefined);

const { esTreeNodeToTSNodeMap } = getParserServices(context);

const tsNode = esTreeNodeToTSNodeMap.get(node) as TSNode & {
type?: TypeNode;
};
return tsNode.type ?? null;
}

/**
* Get the type of the the given node.
*/
export function getTypeDataOfNode<
Context extends RuleContext<string, BaseOptions>,
>(node: TSESTree.Node, context: Context): [Type, TypeNode | null] {
assert(ts !== undefined);

const { esTreeNodeToTSNodeMap } = getParserServices(context);

const tsNode = esTreeNodeToTSNodeMap.get(node) as TSNode & {
type?: TypeNode;
};
return [getTypeOfTSNode(tsNode, context), tsNode.type ?? null];
}

/**
Expand Down Expand Up @@ -274,6 +313,7 @@ export function getTypeImmutabilityOfNode<
// Don't use the global cache in testing environments as it may cause errors when switching between different config options.
process.env["NODE_ENV"] !== "test",
maxImmutability,
typeMatchesPattern,
);
}

Expand Down
81 changes: 81 additions & 0 deletions src/utils/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
type JSONSchema4,
type JSONSchema4ObjectSchema,
} from "@typescript-eslint/utils/json-schema";

const typeSpecifierPatternSchemaProperties: JSONSchema4ObjectSchema["properties"] =
{
name: schemaInstanceOrInstanceArray({
type: "string",
}),
pattern: schemaInstanceOrInstanceArray({
type: "string",
}),
ignoreName: schemaInstanceOrInstanceArray({
type: "string",
}),
ignorePattern: schemaInstanceOrInstanceArray({
type: "string",
}),
};

const typeSpecifierSchema: JSONSchema4 = {
oneOf: [
{
type: "object",
properties: {
...typeSpecifierPatternSchemaProperties,
from: {
type: "string",
enum: ["file"],
},
path: {
type: "string",
},
},
additionalProperties: false,
},
{
type: "object",
properties: {
...typeSpecifierPatternSchemaProperties,
from: {
type: "string",
enum: ["lib"],
},
},
additionalProperties: false,
},
{
type: "object",
properties: {
...typeSpecifierPatternSchemaProperties,
from: {
type: "string",
enum: ["package"],
},
package: {
type: "string",
},
},
additionalProperties: false,
},
],
};

export const typeSpecifiersSchema: JSONSchema4 =
schemaInstanceOrInstanceArray(typeSpecifierSchema);

export function schemaInstanceOrInstanceArray(
items: JSONSchema4,
): NonNullable<JSONSchema4ObjectSchema["properties"]>[string] {
return {
oneOf: [
items,
{
type: "array",
items,
},
],
};
}
Loading

0 comments on commit a8aaa5c

Please sign in to comment.