@cedarjs/eslint-plugin 8.0.0-canary.3546 → 8.0.0-canary.3548
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export declare const rules: {
|
|
|
8
8
|
'process-env-computed': ESLintUtils.RuleModule<"unexpected", [], unknown, ESLintUtils.RuleListener>;
|
|
9
9
|
'service-type-annotations': ESLintUtils.RuleModule<"needsType", [], unknown, ESLintUtils.RuleListener>;
|
|
10
10
|
'unsupported-route-components': ESLintUtils.RuleModule<"unexpected", [], unknown, ESLintUtils.RuleListener>;
|
|
11
|
+
'valid-permission-names': ESLintUtils.RuleModule<"unknownPermission" | "unknownPermissionWithSuggestion", [({
|
|
12
|
+
permissionsFile?: string;
|
|
13
|
+
} | undefined)?], unknown, ESLintUtils.RuleListener>;
|
|
11
14
|
};
|
|
12
15
|
declare const plugin: {
|
|
13
16
|
meta: {
|
|
@@ -19,6 +22,9 @@ declare const plugin: {
|
|
|
19
22
|
'process-env-computed': ESLintUtils.RuleModule<"unexpected", [], unknown, ESLintUtils.RuleListener>;
|
|
20
23
|
'service-type-annotations': ESLintUtils.RuleModule<"needsType", [], unknown, ESLintUtils.RuleListener>;
|
|
21
24
|
'unsupported-route-components': ESLintUtils.RuleModule<"unexpected", [], unknown, ESLintUtils.RuleListener>;
|
|
25
|
+
'valid-permission-names': ESLintUtils.RuleModule<"unknownPermission" | "unknownPermissionWithSuggestion", [({
|
|
26
|
+
permissionsFile?: string;
|
|
27
|
+
} | undefined)?], unknown, ESLintUtils.RuleListener>;
|
|
22
28
|
};
|
|
23
29
|
};
|
|
24
30
|
export default plugin;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAU3D,eAAO,MAAM,IAAI;;;CAGhB,CAAA;AAED,eAAO,MAAM,KAAK;;;;;;;;CAM4D,CAAA;AAE9E,QAAA,MAAM,MAAM;;;;;;;;;;;;;;CAAkB,CAAA;AAE9B,eAAe,MAAM,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { cellTypeAnnotations } from "./cell-type-annotations.js";
|
|
|
3
3
|
import { processEnvComputedRule } from "./process-env-computed.js";
|
|
4
4
|
import { serviceTypeAnnotations } from "./service-type-annotations.js";
|
|
5
5
|
import { unsupportedRouteComponents } from "./unsupported-route-components.js";
|
|
6
|
+
import { validPermissionNames } from "./valid-permission-names.js";
|
|
6
7
|
const meta = {
|
|
7
8
|
name: "@cedarjs/eslint-plugin",
|
|
8
9
|
version: pkgJson.version
|
|
@@ -11,7 +12,8 @@ const rules = {
|
|
|
11
12
|
"cell-type-annotations": cellTypeAnnotations,
|
|
12
13
|
"process-env-computed": processEnvComputedRule,
|
|
13
14
|
"service-type-annotations": serviceTypeAnnotations,
|
|
14
|
-
"unsupported-route-components": unsupportedRouteComponents
|
|
15
|
+
"unsupported-route-components": unsupportedRouteComponents,
|
|
16
|
+
"valid-permission-names": validPermissionNames
|
|
15
17
|
};
|
|
16
18
|
const plugin = { meta, rules };
|
|
17
19
|
var index_default = plugin;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import type { DirectiveNode, StringValueNode } from 'graphql';
|
|
3
|
+
export type UnknownPermission = {
|
|
4
|
+
/** The directive's `permission` argument */
|
|
5
|
+
node: StringValueNode;
|
|
6
|
+
name: string;
|
|
7
|
+
/** The closest defined name, when one is close enough to be a typo */
|
|
8
|
+
suggestion: string | undefined;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* What's wrong with `directive`, or `undefined` when it isn't a
|
|
12
|
+
* `@requirePermission` whose `permission` is a literal that `names` doesn't
|
|
13
|
+
* have
|
|
14
|
+
*/
|
|
15
|
+
export declare function checkRequirePermission(directive: DirectiveNode, names: ReadonlySet<string>): UnknownPermission | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The names in the `permissions: [...]` array of the first
|
|
18
|
+
* `definePermissions({ ... })` call in `source`, or `null` when there's no
|
|
19
|
+
* such call or the array holds anything but string literals (we can't tell
|
|
20
|
+
* what the names are then)
|
|
21
|
+
*/
|
|
22
|
+
export declare function parsePermissionNames(source: string): Set<string> | null;
|
|
23
|
+
export declare const validPermissionNames: ESLintUtils.RuleModule<"unknownPermission" | "unknownPermissionWithSuggestion", [({
|
|
24
|
+
permissionsFile?: string;
|
|
25
|
+
} | undefined)?], unknown, ESLintUtils.RuleListener>;
|
|
26
|
+
//# sourceMappingURL=valid-permission-names.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-permission-names.d.ts","sourceRoot":"","sources":["../src/valid-permission-names.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,WAAW,EAAkB,MAAM,0BAA0B,CAAA;AAEtE,OAAO,KAAK,EAAE,aAAa,EAAgB,eAAe,EAAE,MAAM,SAAS,CAAA;AAkB3E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,4CAA4C;IAC5C,IAAI,EAAE,eAAe,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;CAC/B,CAAA;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,aAAa,EACxB,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GACzB,iBAAiB,GAAG,SAAS,CAqB/B;AAiFD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAiEvE;AA0FD,eAAO,MAAM,oBAAoB;sBACV,MAAM;oDAsG3B,CAAA"}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
simpleTraverse,
|
|
5
|
+
parse as parseTypeScript
|
|
6
|
+
} from "@typescript-eslint/typescript-estree";
|
|
7
|
+
import { ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
8
|
+
import { Kind, parse as parseGraphQL, visit } from "graphql";
|
|
9
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
10
|
+
const PERMISSIONS_FILE = path.join("packages", "permissions", "src", "index.ts");
|
|
11
|
+
const SDL_FILE = /\.sdl\.(?:ts|js)$/;
|
|
12
|
+
function checkRequirePermission(directive, names) {
|
|
13
|
+
if (directive.name.value !== "requirePermission") {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
const argument = directive.arguments?.find(
|
|
17
|
+
(argument2) => argument2.name.value === "permission"
|
|
18
|
+
);
|
|
19
|
+
if (argument?.value.kind !== Kind.STRING) {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
const name = argument.value.value;
|
|
23
|
+
if (names.has(name)) {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
return { node: argument.value, name, suggestion: closestName(name, names) };
|
|
27
|
+
}
|
|
28
|
+
function templateText(template, source) {
|
|
29
|
+
const start = template.range[0] + 1;
|
|
30
|
+
let text = "";
|
|
31
|
+
let cursor = start;
|
|
32
|
+
for (const quasi of template.quasis) {
|
|
33
|
+
const rawStart = quasi.range[0] + 1;
|
|
34
|
+
const rawEnd = quasi.range[1] - (quasi.tail ? 1 : 2);
|
|
35
|
+
text += source.slice(cursor, rawStart).replace(/[^\n]/g, " ");
|
|
36
|
+
text += source.slice(rawStart, rawEnd);
|
|
37
|
+
cursor = rawEnd;
|
|
38
|
+
}
|
|
39
|
+
return { start, text };
|
|
40
|
+
}
|
|
41
|
+
const cache = /* @__PURE__ */ new Map();
|
|
42
|
+
function unwrapTypeExpressions(node) {
|
|
43
|
+
let current = node;
|
|
44
|
+
while (current.type === AST_NODE_TYPES.TSAsExpression || current.type === AST_NODE_TYPES.TSSatisfiesExpression || current.type === AST_NODE_TYPES.TSNonNullExpression) {
|
|
45
|
+
current = current.expression;
|
|
46
|
+
}
|
|
47
|
+
return current;
|
|
48
|
+
}
|
|
49
|
+
function stringValue(node) {
|
|
50
|
+
if (node.type === AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
51
|
+
return node.value;
|
|
52
|
+
}
|
|
53
|
+
if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
54
|
+
return node.quasis[0].value.cooked ?? void 0;
|
|
55
|
+
}
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
function propertyName(property) {
|
|
59
|
+
if (property.computed) {
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
if (property.key.type === AST_NODE_TYPES.Identifier) {
|
|
63
|
+
return property.key.name;
|
|
64
|
+
}
|
|
65
|
+
return stringValue(property.key);
|
|
66
|
+
}
|
|
67
|
+
function parsePermissionNames(source) {
|
|
68
|
+
let ast;
|
|
69
|
+
try {
|
|
70
|
+
ast = parseTypeScript(source, { jsx: false, range: false, loc: false });
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const result = { names: null };
|
|
75
|
+
let found = false;
|
|
76
|
+
simpleTraverse(ast, {
|
|
77
|
+
enter(node) {
|
|
78
|
+
if (found || node.type !== AST_NODE_TYPES.CallExpression || node.callee.type !== AST_NODE_TYPES.Identifier || node.callee.name !== "definePermissions") {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
found = true;
|
|
82
|
+
const options = node.arguments[0];
|
|
83
|
+
if (options?.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const property of options.properties) {
|
|
87
|
+
if (property.type !== AST_NODE_TYPES.Property || propertyName(property) !== "permissions") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const value = unwrapTypeExpressions(property.value);
|
|
91
|
+
if (value.type !== AST_NODE_TYPES.ArrayExpression) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const names = /* @__PURE__ */ new Set();
|
|
95
|
+
for (const element of value.elements) {
|
|
96
|
+
const name = element ? stringValue(unwrapTypeExpressions(element)) : void 0;
|
|
97
|
+
if (name === void 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
names.add(name);
|
|
101
|
+
}
|
|
102
|
+
result.names = names;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return result.names;
|
|
107
|
+
}
|
|
108
|
+
function readPermissionNames(file) {
|
|
109
|
+
let mtimeMs;
|
|
110
|
+
try {
|
|
111
|
+
mtimeMs = fs.statSync(file).mtimeMs;
|
|
112
|
+
} catch {
|
|
113
|
+
cache.delete(file);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const cached = cache.get(file);
|
|
117
|
+
if (cached?.mtimeMs === mtimeMs) {
|
|
118
|
+
return cached.names;
|
|
119
|
+
}
|
|
120
|
+
let names = null;
|
|
121
|
+
try {
|
|
122
|
+
names = parsePermissionNames(fs.readFileSync(file, "utf-8"));
|
|
123
|
+
} catch {
|
|
124
|
+
names = null;
|
|
125
|
+
}
|
|
126
|
+
cache.set(file, { mtimeMs, names });
|
|
127
|
+
return names;
|
|
128
|
+
}
|
|
129
|
+
function findPermissionsFile(dir) {
|
|
130
|
+
for (let current = dir; ; ) {
|
|
131
|
+
const candidate = path.join(current, PERMISSIONS_FILE);
|
|
132
|
+
if (fs.existsSync(candidate)) {
|
|
133
|
+
return candidate;
|
|
134
|
+
}
|
|
135
|
+
const parent = path.dirname(current);
|
|
136
|
+
if (parent === current) {
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
current = parent;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function editDistance(a, b) {
|
|
143
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
144
|
+
for (let i = 1; i <= a.length; i++) {
|
|
145
|
+
const row = [i];
|
|
146
|
+
for (let j = 1; j <= b.length; j++) {
|
|
147
|
+
row[j] = Math.min(
|
|
148
|
+
previous[j] + 1,
|
|
149
|
+
row[j - 1] + 1,
|
|
150
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
previous = row;
|
|
154
|
+
}
|
|
155
|
+
return previous[b.length];
|
|
156
|
+
}
|
|
157
|
+
function closestName(name, names) {
|
|
158
|
+
const maxDistance = Math.max(2, Math.floor(name.length / 4));
|
|
159
|
+
let best;
|
|
160
|
+
for (const candidate of names) {
|
|
161
|
+
const distance = editDistance(name, candidate);
|
|
162
|
+
if (distance <= maxDistance && (!best || distance < best.distance)) {
|
|
163
|
+
best = { name: candidate, distance };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return best?.name;
|
|
167
|
+
}
|
|
168
|
+
const validPermissionNames = createRule({
|
|
169
|
+
meta: {
|
|
170
|
+
type: "problem",
|
|
171
|
+
docs: {
|
|
172
|
+
description: "Check that the permissions named in an SDL file's @requirePermission directives are defined"
|
|
173
|
+
},
|
|
174
|
+
messages: {
|
|
175
|
+
unknownPermission: 'Unknown permission "{{name}}". Add it to `permissions` in definePermissions(), or fix the name.',
|
|
176
|
+
unknownPermissionWithSuggestion: 'Unknown permission "{{name}}". Did you mean "{{suggestion}}"?'
|
|
177
|
+
},
|
|
178
|
+
schema: [
|
|
179
|
+
{
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
permissionsFile: { type: "string" }
|
|
183
|
+
},
|
|
184
|
+
additionalProperties: false
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
defaultOptions: [{}],
|
|
189
|
+
create(context, [options = {}]) {
|
|
190
|
+
if (!SDL_FILE.test(context.filename)) {
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
const permissionsFile = options.permissionsFile ? path.resolve(context.cwd, options.permissionsFile) : findPermissionsFile(path.dirname(context.filename));
|
|
194
|
+
const names = permissionsFile ? readPermissionNames(permissionsFile) : null;
|
|
195
|
+
if (!names) {
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
const { sourceCode } = context;
|
|
199
|
+
return {
|
|
200
|
+
TaggedTemplateExpression(node) {
|
|
201
|
+
if (node.tag.type !== AST_NODE_TYPES.Identifier || node.tag.name !== "gql") {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const { start, text } = templateText(node.quasi, sourceCode.text);
|
|
205
|
+
let document;
|
|
206
|
+
try {
|
|
207
|
+
document = parseGraphQL(text);
|
|
208
|
+
} catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
visit(document, {
|
|
212
|
+
Directive(directive) {
|
|
213
|
+
const unknown = checkRequirePermission(directive, names);
|
|
214
|
+
if (!unknown?.node.loc) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const { name, suggestion } = unknown;
|
|
218
|
+
const { loc } = unknown.node;
|
|
219
|
+
if (sourceCode.text.slice(start + loc.start, start + loc.end) !== text.slice(loc.start, loc.end)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
context.report({
|
|
223
|
+
loc: {
|
|
224
|
+
start: sourceCode.getLocFromIndex(start + loc.start),
|
|
225
|
+
end: sourceCode.getLocFromIndex(start + loc.end)
|
|
226
|
+
},
|
|
227
|
+
...suggestion ? {
|
|
228
|
+
messageId: "unknownPermissionWithSuggestion",
|
|
229
|
+
data: { name, suggestion }
|
|
230
|
+
} : {
|
|
231
|
+
messageId: "unknownPermission",
|
|
232
|
+
data: { name }
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
export {
|
|
242
|
+
checkRequirePermission,
|
|
243
|
+
parsePermissionNames,
|
|
244
|
+
validPermissionNames
|
|
245
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/eslint-plugin",
|
|
3
|
-
"version": "8.0.0-canary.
|
|
3
|
+
"version": "8.0.0-canary.3548",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/cedarjs/cedar.git",
|
|
@@ -23,15 +23,18 @@
|
|
|
23
23
|
"test:watch": "vitest watch"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@typescript-eslint/typescript-estree": "8.72.0",
|
|
26
27
|
"@typescript-eslint/utils": "8.72.0",
|
|
27
|
-
"eslint": "8.57.1"
|
|
28
|
+
"eslint": "8.57.1",
|
|
29
|
+
"graphql": "16.14.2"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
|
-
"@cedarjs/framework-tools": "8.0.0-canary.
|
|
32
|
+
"@cedarjs/framework-tools": "8.0.0-canary.3548",
|
|
31
33
|
"@types/eslint": "8.56.12",
|
|
32
34
|
"@types/estree": "1.0.9",
|
|
33
35
|
"@typescript-eslint/parser": "8.72.0",
|
|
34
36
|
"@typescript-eslint/rule-tester": "8.72.0",
|
|
37
|
+
"ts-dedent": "2.3.0",
|
|
35
38
|
"typescript": "5.9.3",
|
|
36
39
|
"vitest": "4.1.12"
|
|
37
40
|
},
|