@sanity/lint-core 0.0.1 → 0.0.3
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.cjs +91 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +27 -0
- package/dist/testing.cjs +197 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +124 -0
- package/dist/types-CPtvWKni.d.cts +112 -0
- package/package.json +7 -5
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
formatFindings: () => formatFindings,
|
|
24
|
+
formatFindingsJson: () => formatFindingsJson,
|
|
25
|
+
summarizeFindings: () => summarizeFindings
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/reporter.ts
|
|
30
|
+
function formatFindings(query, findings) {
|
|
31
|
+
if (findings.length === 0) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
const lines = query.split("\n");
|
|
35
|
+
const output = [];
|
|
36
|
+
for (const finding of findings) {
|
|
37
|
+
const severityPrefix = getSeverityPrefix(finding.severity);
|
|
38
|
+
output.push(`${severityPrefix}[${finding.ruleId}]: ${finding.message}`);
|
|
39
|
+
if (finding.span) {
|
|
40
|
+
const { line, column } = finding.span.start;
|
|
41
|
+
output.push(` --> query:${line}:${column}`);
|
|
42
|
+
const lineContent = lines[line - 1];
|
|
43
|
+
if (lineContent !== void 0) {
|
|
44
|
+
const lineNum = String(line).padStart(3, " ");
|
|
45
|
+
output.push(` |`);
|
|
46
|
+
output.push(`${lineNum} | ${lineContent}`);
|
|
47
|
+
const caretPadding = " ".repeat(column - 1);
|
|
48
|
+
const caretLength = Math.max(
|
|
49
|
+
1,
|
|
50
|
+
finding.span.end.line === line ? finding.span.end.column - column : lineContent.length - column + 1
|
|
51
|
+
);
|
|
52
|
+
const caret = "^".repeat(caretLength);
|
|
53
|
+
output.push(` | ${caretPadding}${caret}`);
|
|
54
|
+
}
|
|
55
|
+
output.push(` |`);
|
|
56
|
+
}
|
|
57
|
+
if (finding.help) {
|
|
58
|
+
output.push(` = help: ${finding.help}`);
|
|
59
|
+
}
|
|
60
|
+
output.push("");
|
|
61
|
+
}
|
|
62
|
+
return output.join("\n");
|
|
63
|
+
}
|
|
64
|
+
function formatFindingsJson(findings) {
|
|
65
|
+
return JSON.stringify(findings, null, 2);
|
|
66
|
+
}
|
|
67
|
+
function getSeverityPrefix(severity) {
|
|
68
|
+
switch (severity) {
|
|
69
|
+
case "error":
|
|
70
|
+
return "error";
|
|
71
|
+
case "warning":
|
|
72
|
+
return "warning";
|
|
73
|
+
case "info":
|
|
74
|
+
return "info";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function summarizeFindings(findings) {
|
|
78
|
+
return {
|
|
79
|
+
total: findings.length,
|
|
80
|
+
errors: findings.filter((f) => f.severity === "error").length,
|
|
81
|
+
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
82
|
+
infos: findings.filter((f) => f.severity === "info").length
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
86
|
+
0 && (module.exports = {
|
|
87
|
+
formatFindings,
|
|
88
|
+
formatFindingsJson,
|
|
89
|
+
summarizeFindings
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/reporter.ts"],"sourcesContent":["// Types\nexport type {\n Severity,\n Category,\n SourceLocation,\n SourceSpan,\n Suggestion,\n Finding,\n RuleContext,\n Rule,\n RuleConfig,\n LinterConfig,\n} from './types'\n\n// Re-export SchemaType from groq-js for convenience\nexport type { SchemaType } from 'groq-js'\n\n// Reporting utilities\nexport { formatFindings, formatFindingsJson, summarizeFindings } from './reporter'\nexport type { FindingsSummary } from './reporter'\n\n// Note: RuleTester is exported from '@sanity/lint-core/testing' to avoid\n// importing vitest in production code\n","import type { Finding } from './types'\n\n/**\n * Format findings as a human-readable string\n */\nexport function formatFindings(query: string, findings: Finding[]): string {\n if (findings.length === 0) {\n return ''\n }\n\n const lines = query.split('\\n')\n const output: string[] = []\n\n for (const finding of findings) {\n const severityPrefix = getSeverityPrefix(finding.severity)\n\n output.push(`${severityPrefix}[${finding.ruleId}]: ${finding.message}`)\n\n if (finding.span) {\n const { line, column } = finding.span.start\n output.push(` --> query:${line}:${column}`)\n\n // Show the offending line with context\n const lineContent = lines[line - 1]\n if (lineContent !== undefined) {\n const lineNum = String(line).padStart(3, ' ')\n output.push(` |`)\n output.push(`${lineNum} | ${lineContent}`)\n\n // Add caret pointing to the issue\n const caretPadding = ' '.repeat(column - 1)\n const caretLength = Math.max(\n 1,\n finding.span.end.line === line\n ? finding.span.end.column - column\n : lineContent.length - column + 1\n )\n const caret = '^'.repeat(caretLength)\n output.push(` | ${caretPadding}${caret}`)\n }\n output.push(` |`)\n }\n\n if (finding.help) {\n output.push(` = help: ${finding.help}`)\n }\n\n output.push('')\n }\n\n return output.join('\\n')\n}\n\n/**\n * Format findings as JSON\n */\nexport function formatFindingsJson(findings: Finding[]): string {\n return JSON.stringify(findings, null, 2)\n}\n\n/**\n * Get severity prefix for display\n */\nfunction getSeverityPrefix(severity: Finding['severity']): string {\n switch (severity) {\n case 'error':\n return 'error'\n case 'warning':\n return 'warning'\n case 'info':\n return 'info'\n }\n}\n\n/**\n * Summary of findings by severity\n */\nexport interface FindingsSummary {\n total: number\n errors: number\n warnings: number\n infos: number\n}\n\n/**\n * Get a summary of findings\n */\nexport function summarizeFindings(findings: Finding[]): FindingsSummary {\n return {\n total: findings.length,\n errors: findings.filter((f) => f.severity === 'error').length,\n warnings: findings.filter((f) => f.severity === 'warning').length,\n infos: findings.filter((f) => f.severity === 'info').length,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,SAAS,eAAe,OAAe,UAA6B;AACzE,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,QAAM,SAAmB,CAAC;AAE1B,aAAW,WAAW,UAAU;AAC9B,UAAM,iBAAiB,kBAAkB,QAAQ,QAAQ;AAEzD,WAAO,KAAK,GAAG,cAAc,IAAI,QAAQ,MAAM,MAAM,QAAQ,OAAO,EAAE;AAEtE,QAAI,QAAQ,MAAM;AAChB,YAAM,EAAE,MAAM,OAAO,IAAI,QAAQ,KAAK;AACtC,aAAO,KAAK,eAAe,IAAI,IAAI,MAAM,EAAE;AAG3C,YAAM,cAAc,MAAM,OAAO,CAAC;AAClC,UAAI,gBAAgB,QAAW;AAC7B,cAAM,UAAU,OAAO,IAAI,EAAE,SAAS,GAAG,GAAG;AAC5C,eAAO,KAAK,MAAM;AAClB,eAAO,KAAK,GAAG,OAAO,MAAM,WAAW,EAAE;AAGzC,cAAM,eAAe,IAAI,OAAO,SAAS,CAAC;AAC1C,cAAM,cAAc,KAAK;AAAA,UACvB;AAAA,UACA,QAAQ,KAAK,IAAI,SAAS,OACtB,QAAQ,KAAK,IAAI,SAAS,SAC1B,YAAY,SAAS,SAAS;AAAA,QACpC;AACA,cAAM,QAAQ,IAAI,OAAO,WAAW;AACpC,eAAO,KAAK,QAAQ,YAAY,GAAG,KAAK,EAAE;AAAA,MAC5C;AACA,aAAO,KAAK,MAAM;AAAA,IACpB;AAEA,QAAI,QAAQ,MAAM;AAChB,aAAO,KAAK,aAAa,QAAQ,IAAI,EAAE;AAAA,IACzC;AAEA,WAAO,KAAK,EAAE;AAAA,EAChB;AAEA,SAAO,OAAO,KAAK,IAAI;AACzB;AAKO,SAAS,mBAAmB,UAA6B;AAC9D,SAAO,KAAK,UAAU,UAAU,MAAM,CAAC;AACzC;AAKA,SAAS,kBAAkB,UAAuC;AAChE,UAAQ,UAAU;AAAA,IAChB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EACX;AACF;AAeO,SAAS,kBAAkB,UAAsC;AACtE,SAAO;AAAA,IACL,OAAO,SAAS;AAAA,IAChB,QAAQ,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO,EAAE;AAAA,IACvD,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,SAAS,EAAE;AAAA,IAC3D,OAAO,SAAS,OAAO,CAAC,MAAM,EAAE,aAAa,MAAM,EAAE;AAAA,EACvD;AACF;","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { F as Finding } from './types-CPtvWKni.cjs';
|
|
2
|
+
export { C as Category, L as LinterConfig, d as Rule, e as RuleConfig, R as RuleContext, S as Severity, a as SourceLocation, b as SourceSpan, c as Suggestion } from './types-CPtvWKni.cjs';
|
|
3
|
+
export { SchemaType } from 'groq-js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format findings as a human-readable string
|
|
7
|
+
*/
|
|
8
|
+
declare function formatFindings(query: string, findings: Finding[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* Format findings as JSON
|
|
11
|
+
*/
|
|
12
|
+
declare function formatFindingsJson(findings: Finding[]): string;
|
|
13
|
+
/**
|
|
14
|
+
* Summary of findings by severity
|
|
15
|
+
*/
|
|
16
|
+
interface FindingsSummary {
|
|
17
|
+
total: number;
|
|
18
|
+
errors: number;
|
|
19
|
+
warnings: number;
|
|
20
|
+
infos: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get a summary of findings
|
|
24
|
+
*/
|
|
25
|
+
declare function summarizeFindings(findings: Finding[]): FindingsSummary;
|
|
26
|
+
|
|
27
|
+
export { Finding, type FindingsSummary, formatFindings, formatFindingsJson, summarizeFindings };
|
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/testing.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
RuleTester: () => RuleTester,
|
|
24
|
+
assertValidGroq: () => assertValidGroq,
|
|
25
|
+
isValidGroq: () => isValidGroq,
|
|
26
|
+
parseGroq: () => parseGroq
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(testing_exports);
|
|
29
|
+
|
|
30
|
+
// src/rule-tester.ts
|
|
31
|
+
var import_vitest = require("vitest");
|
|
32
|
+
var import_groq_js = require("groq-js");
|
|
33
|
+
function runRule(rule, query) {
|
|
34
|
+
const findings = [];
|
|
35
|
+
let ast;
|
|
36
|
+
try {
|
|
37
|
+
ast = (0, import_groq_js.parse)(query);
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const context = {
|
|
42
|
+
query,
|
|
43
|
+
queryLength: query.length,
|
|
44
|
+
report: (finding) => {
|
|
45
|
+
findings.push({
|
|
46
|
+
...finding,
|
|
47
|
+
ruleId: rule.id
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
rule.check(ast, context);
|
|
52
|
+
return findings;
|
|
53
|
+
}
|
|
54
|
+
function assertFindingMatches(finding, expected, ruleId) {
|
|
55
|
+
const expectedRuleId = expected.ruleId ?? ruleId;
|
|
56
|
+
(0, import_vitest.expect)(finding.ruleId).toBe(expectedRuleId);
|
|
57
|
+
if (expected.message !== void 0) {
|
|
58
|
+
if (typeof expected.message === "string") {
|
|
59
|
+
(0, import_vitest.expect)(finding.message).toBe(expected.message);
|
|
60
|
+
} else {
|
|
61
|
+
(0, import_vitest.expect)(finding.message).toMatch(expected.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (expected.severity !== void 0) {
|
|
65
|
+
(0, import_vitest.expect)(finding.severity).toBe(expected.severity);
|
|
66
|
+
}
|
|
67
|
+
if (expected.line !== void 0 && finding.span) {
|
|
68
|
+
(0, import_vitest.expect)(finding.span.start.line).toBe(expected.line);
|
|
69
|
+
}
|
|
70
|
+
if (expected.column !== void 0 && finding.span) {
|
|
71
|
+
(0, import_vitest.expect)(finding.span.start.column).toBe(expected.column);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
var RuleTester = class {
|
|
75
|
+
/**
|
|
76
|
+
* Run tests for a rule
|
|
77
|
+
* @param ruleName - Name for the test suite
|
|
78
|
+
* @param rule - The rule to test
|
|
79
|
+
* @param tests - Valid and invalid test cases
|
|
80
|
+
*/
|
|
81
|
+
run(ruleName, rule, tests) {
|
|
82
|
+
(0, import_vitest.describe)(ruleName, () => {
|
|
83
|
+
(0, import_vitest.describe)("valid", () => {
|
|
84
|
+
for (const test of tests.valid) {
|
|
85
|
+
const testCase = typeof test === "string" ? { code: test } : test;
|
|
86
|
+
const testName = testCase.name ?? this.truncate(testCase.code, 60);
|
|
87
|
+
(0, import_vitest.it)(testName, () => {
|
|
88
|
+
const findings = runRule(rule, testCase.code);
|
|
89
|
+
(0, import_vitest.expect)(findings).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
(0, import_vitest.describe)("invalid", () => {
|
|
94
|
+
for (const test of tests.invalid) {
|
|
95
|
+
const testName = test.name ?? this.truncate(test.code, 60);
|
|
96
|
+
(0, import_vitest.it)(testName, () => {
|
|
97
|
+
const findings = runRule(rule, test.code);
|
|
98
|
+
(0, import_vitest.expect)(findings).toHaveLength(test.errors.length);
|
|
99
|
+
for (let i = 0; i < test.errors.length; i++) {
|
|
100
|
+
const finding = findings[i];
|
|
101
|
+
const expected = test.errors[i];
|
|
102
|
+
if (finding && expected) {
|
|
103
|
+
assertFindingMatches(finding, expected, rule.id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Truncate a string for display
|
|
113
|
+
*/
|
|
114
|
+
truncate(str, maxLength) {
|
|
115
|
+
const oneLine = str.replace(/\s+/g, " ").trim();
|
|
116
|
+
if (oneLine.length <= maxLength) {
|
|
117
|
+
return oneLine;
|
|
118
|
+
}
|
|
119
|
+
return oneLine.slice(0, maxLength - 3) + "...";
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// src/groq-validator.ts
|
|
124
|
+
var import_groq_js2 = require("groq-js");
|
|
125
|
+
function parseGroq(query) {
|
|
126
|
+
const trimmed = query.trim();
|
|
127
|
+
if (!trimmed) {
|
|
128
|
+
const error = {
|
|
129
|
+
message: "Empty GROQ query",
|
|
130
|
+
query
|
|
131
|
+
};
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
return (0, import_groq_js2.parse)(trimmed);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
const error = {
|
|
138
|
+
message: e instanceof Error ? e.message : String(e),
|
|
139
|
+
query
|
|
140
|
+
};
|
|
141
|
+
const posMatch = error.message.match(/position (\d+)/);
|
|
142
|
+
if (posMatch && posMatch[1]) {
|
|
143
|
+
error.position = parseInt(posMatch[1], 10);
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function isValidGroq(query) {
|
|
149
|
+
try {
|
|
150
|
+
parseGroq(query);
|
|
151
|
+
return true;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function assertValidGroq(query, context) {
|
|
157
|
+
try {
|
|
158
|
+
parseGroq(query);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
const error = e;
|
|
161
|
+
let message = `Invalid GROQ syntax: ${error.message}`;
|
|
162
|
+
if (context) {
|
|
163
|
+
message = `${context}: ${message}`;
|
|
164
|
+
}
|
|
165
|
+
if (error.position !== void 0) {
|
|
166
|
+
const lines = error.query.split("\n");
|
|
167
|
+
let charCount = 0;
|
|
168
|
+
let errorCol = 0;
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
if (charCount + line.length >= error.position) {
|
|
171
|
+
errorCol = error.position - charCount;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
charCount += line.length + 1;
|
|
175
|
+
}
|
|
176
|
+
message += `
|
|
177
|
+
|
|
178
|
+
Query:
|
|
179
|
+
${error.query}
|
|
180
|
+
${" ".repeat(errorCol)}^ position ${error.position}`;
|
|
181
|
+
} else {
|
|
182
|
+
message += `
|
|
183
|
+
|
|
184
|
+
Query:
|
|
185
|
+
${error.query}`;
|
|
186
|
+
}
|
|
187
|
+
throw new Error(message);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
191
|
+
0 && (module.exports = {
|
|
192
|
+
RuleTester,
|
|
193
|
+
assertValidGroq,
|
|
194
|
+
isValidGroq,
|
|
195
|
+
parseGroq
|
|
196
|
+
});
|
|
197
|
+
//# sourceMappingURL=testing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/testing.ts","../src/rule-tester.ts","../src/groq-validator.ts"],"sourcesContent":["// Testing utilities - this file imports vitest\n// Import from '@sanity/lint-core/testing' in test files\n\nexport { RuleTester } from './rule-tester'\nexport type { ValidTestCase, InvalidTestCase, ExpectedError, RuleTests } from './rule-tester'\n\n// GROQ validation utilities\n// These use groq-js as the single source of truth for GROQ syntax validation\nexport { assertValidGroq, isValidGroq, parseGroq, type GroqParseError } from './groq-validator'\n","import { describe, it, expect } from 'vitest'\nimport { parse } from 'groq-js'\nimport type { Rule, Finding, RuleContext } from './types'\n\n/**\n * A valid test case - query that should produce no findings\n */\nexport interface ValidTestCase {\n /** The GROQ query to test */\n code: string\n /** Optional name for the test */\n name?: string\n}\n\n/**\n * Expected error in an invalid test case\n */\nexport interface ExpectedError {\n /** Expected rule ID (defaults to the rule being tested) */\n ruleId?: string\n /** Expected message (string for exact match, RegExp for pattern) */\n message?: string | RegExp\n /** Expected severity */\n severity?: 'error' | 'warning' | 'info'\n /** Expected line number (1-based) */\n line?: number\n /** Expected column number (1-based) */\n column?: number\n}\n\n/**\n * An invalid test case - query that should produce findings\n */\nexport interface InvalidTestCase {\n /** The GROQ query to test */\n code: string\n /** Optional name for the test */\n name?: string\n /** Expected errors */\n errors: ExpectedError[]\n}\n\n/**\n * Test suite for a rule\n */\nexport interface RuleTests {\n /** Queries that should produce no findings */\n valid: (string | ValidTestCase)[]\n /** Queries that should produce findings */\n invalid: InvalidTestCase[]\n}\n\n/**\n * Run a single rule against a query and collect findings\n */\nfunction runRule(rule: Rule, query: string): Finding[] {\n const findings: Finding[] = []\n\n let ast\n try {\n ast = parse(query)\n } catch {\n // If query doesn't parse, return empty findings\n // (parser errors are separate from lint errors)\n return []\n }\n\n const context: RuleContext = {\n query,\n queryLength: query.length,\n report: (finding) => {\n findings.push({\n ...finding,\n ruleId: rule.id,\n })\n },\n }\n\n rule.check(ast, context)\n\n return findings\n}\n\n/**\n * Assert that a finding matches expected error\n */\nfunction assertFindingMatches(finding: Finding, expected: ExpectedError, ruleId: string): void {\n // Check rule ID\n const expectedRuleId = expected.ruleId ?? ruleId\n expect(finding.ruleId).toBe(expectedRuleId)\n\n // Check message if specified\n if (expected.message !== undefined) {\n if (typeof expected.message === 'string') {\n expect(finding.message).toBe(expected.message)\n } else {\n expect(finding.message).toMatch(expected.message)\n }\n }\n\n // Check severity if specified\n if (expected.severity !== undefined) {\n expect(finding.severity).toBe(expected.severity)\n }\n\n // Check location if specified\n if (expected.line !== undefined && finding.span) {\n expect(finding.span.start.line).toBe(expected.line)\n }\n if (expected.column !== undefined && finding.span) {\n expect(finding.span.start.column).toBe(expected.column)\n }\n}\n\n/**\n * Test utility for lint rules, inspired by ESLint's RuleTester\n *\n * @example\n * ```typescript\n * import { RuleTester } from '@sanity/lint-core'\n * import { joinInFilter } from '../join-in-filter'\n *\n * const tester = new RuleTester()\n *\n * tester.run('join-in-filter', joinInFilter, {\n * valid: [\n * '*[_type == \"post\"]',\n * '*[_type == \"post\"]{ author-> }',\n * ],\n * invalid: [\n * {\n * code: '*[author->name == \"Bob\"]',\n * errors: [{ ruleId: 'join-in-filter' }]\n * }\n * ]\n * })\n * ```\n */\nexport class RuleTester {\n /**\n * Run tests for a rule\n * @param ruleName - Name for the test suite\n * @param rule - The rule to test\n * @param tests - Valid and invalid test cases\n */\n run(ruleName: string, rule: Rule, tests: RuleTests): void {\n describe(ruleName, () => {\n describe('valid', () => {\n for (const test of tests.valid) {\n const testCase = typeof test === 'string' ? { code: test } : test\n const testName = testCase.name ?? this.truncate(testCase.code, 60)\n\n it(testName, () => {\n const findings = runRule(rule, testCase.code)\n expect(findings).toHaveLength(0)\n })\n }\n })\n\n describe('invalid', () => {\n for (const test of tests.invalid) {\n const testName = test.name ?? this.truncate(test.code, 60)\n\n it(testName, () => {\n const findings = runRule(rule, test.code)\n\n // Check we got the expected number of errors\n expect(findings).toHaveLength(test.errors.length)\n\n // Check each error matches\n for (let i = 0; i < test.errors.length; i++) {\n const finding = findings[i]\n const expected = test.errors[i]\n if (finding && expected) {\n assertFindingMatches(finding, expected, rule.id)\n }\n }\n })\n }\n })\n })\n }\n\n /**\n * Truncate a string for display\n */\n private truncate(str: string, maxLength: number): string {\n const oneLine = str.replace(/\\s+/g, ' ').trim()\n if (oneLine.length <= maxLength) {\n return oneLine\n }\n return oneLine.slice(0, maxLength - 3) + '...'\n }\n}\n","/**\n * GROQ validation utilities using groq-js as the single source of truth.\n *\n * Use these in tests to ensure queries are valid GROQ syntax:\n *\n * ```ts\n * import { assertValidGroq } from '@sanity/lint-core/testing'\n *\n * it('produces valid GROQ', () => {\n * const result = formatGroq(query)\n * assertValidGroq(result) // Throws if invalid\n * })\n * ```\n */\n\nimport { parse, type ExprNode } from 'groq-js'\n\nexport interface GroqParseError {\n message: string\n position?: number\n query: string\n}\n\n/**\n * Parse a GROQ query string and return the AST.\n * Throws a GroqParseError if the query is invalid.\n */\nexport function parseGroq(query: string): ExprNode {\n const trimmed = query.trim()\n if (!trimmed) {\n const error: GroqParseError = {\n message: 'Empty GROQ query',\n query,\n }\n throw error\n }\n\n try {\n return parse(trimmed)\n } catch (e) {\n const error: GroqParseError = {\n message: e instanceof Error ? e.message : String(e),\n query,\n }\n // Extract position from groq-js error message if available\n const posMatch = error.message.match(/position (\\d+)/)\n if (posMatch && posMatch[1]) {\n error.position = parseInt(posMatch[1], 10)\n }\n throw error\n }\n}\n\n/**\n * Check if a string is valid GROQ syntax.\n * Returns true if valid, false if invalid.\n */\nexport function isValidGroq(query: string): boolean {\n try {\n parseGroq(query)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Assert that a string is valid GROQ syntax.\n * Throws an error with helpful context if invalid.\n *\n * Use this in tests to validate formatter output or generated queries.\n */\nexport function assertValidGroq(query: string, context?: string): void {\n try {\n parseGroq(query)\n } catch (e) {\n const error = e as GroqParseError\n let message = `Invalid GROQ syntax: ${error.message}`\n if (context) {\n message = `${context}: ${message}`\n }\n if (error.position !== undefined) {\n // Show the query with a pointer to the error position\n const lines = error.query.split('\\n')\n let charCount = 0\n let errorCol = 0\n\n for (const line of lines) {\n if (charCount + line.length >= error.position) {\n errorCol = error.position - charCount\n break\n }\n charCount += line.length + 1 // +1 for newline\n }\n\n message += `\\n\\nQuery:\\n${error.query}\\n${' '.repeat(errorCol)}^ position ${error.position}`\n } else {\n message += `\\n\\nQuery:\\n${error.query}`\n }\n\n throw new Error(message)\n }\n}\n"],"mappings":"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","names":["import_groq_js"]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { d as Rule } from './types-CPtvWKni.cjs';
|
|
2
|
+
import { ExprNode } from 'groq-js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A valid test case - query that should produce no findings
|
|
6
|
+
*/
|
|
7
|
+
interface ValidTestCase {
|
|
8
|
+
/** The GROQ query to test */
|
|
9
|
+
code: string;
|
|
10
|
+
/** Optional name for the test */
|
|
11
|
+
name?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Expected error in an invalid test case
|
|
15
|
+
*/
|
|
16
|
+
interface ExpectedError {
|
|
17
|
+
/** Expected rule ID (defaults to the rule being tested) */
|
|
18
|
+
ruleId?: string;
|
|
19
|
+
/** Expected message (string for exact match, RegExp for pattern) */
|
|
20
|
+
message?: string | RegExp;
|
|
21
|
+
/** Expected severity */
|
|
22
|
+
severity?: 'error' | 'warning' | 'info';
|
|
23
|
+
/** Expected line number (1-based) */
|
|
24
|
+
line?: number;
|
|
25
|
+
/** Expected column number (1-based) */
|
|
26
|
+
column?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* An invalid test case - query that should produce findings
|
|
30
|
+
*/
|
|
31
|
+
interface InvalidTestCase {
|
|
32
|
+
/** The GROQ query to test */
|
|
33
|
+
code: string;
|
|
34
|
+
/** Optional name for the test */
|
|
35
|
+
name?: string;
|
|
36
|
+
/** Expected errors */
|
|
37
|
+
errors: ExpectedError[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Test suite for a rule
|
|
41
|
+
*/
|
|
42
|
+
interface RuleTests {
|
|
43
|
+
/** Queries that should produce no findings */
|
|
44
|
+
valid: (string | ValidTestCase)[];
|
|
45
|
+
/** Queries that should produce findings */
|
|
46
|
+
invalid: InvalidTestCase[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Test utility for lint rules, inspired by ESLint's RuleTester
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import { RuleTester } from '@sanity/lint-core'
|
|
54
|
+
* import { joinInFilter } from '../join-in-filter'
|
|
55
|
+
*
|
|
56
|
+
* const tester = new RuleTester()
|
|
57
|
+
*
|
|
58
|
+
* tester.run('join-in-filter', joinInFilter, {
|
|
59
|
+
* valid: [
|
|
60
|
+
* '*[_type == "post"]',
|
|
61
|
+
* '*[_type == "post"]{ author-> }',
|
|
62
|
+
* ],
|
|
63
|
+
* invalid: [
|
|
64
|
+
* {
|
|
65
|
+
* code: '*[author->name == "Bob"]',
|
|
66
|
+
* errors: [{ ruleId: 'join-in-filter' }]
|
|
67
|
+
* }
|
|
68
|
+
* ]
|
|
69
|
+
* })
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare class RuleTester {
|
|
73
|
+
/**
|
|
74
|
+
* Run tests for a rule
|
|
75
|
+
* @param ruleName - Name for the test suite
|
|
76
|
+
* @param rule - The rule to test
|
|
77
|
+
* @param tests - Valid and invalid test cases
|
|
78
|
+
*/
|
|
79
|
+
run(ruleName: string, rule: Rule, tests: RuleTests): void;
|
|
80
|
+
/**
|
|
81
|
+
* Truncate a string for display
|
|
82
|
+
*/
|
|
83
|
+
private truncate;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* GROQ validation utilities using groq-js as the single source of truth.
|
|
88
|
+
*
|
|
89
|
+
* Use these in tests to ensure queries are valid GROQ syntax:
|
|
90
|
+
*
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { assertValidGroq } from '@sanity/lint-core/testing'
|
|
93
|
+
*
|
|
94
|
+
* it('produces valid GROQ', () => {
|
|
95
|
+
* const result = formatGroq(query)
|
|
96
|
+
* assertValidGroq(result) // Throws if invalid
|
|
97
|
+
* })
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
interface GroqParseError {
|
|
102
|
+
message: string;
|
|
103
|
+
position?: number;
|
|
104
|
+
query: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Parse a GROQ query string and return the AST.
|
|
108
|
+
* Throws a GroqParseError if the query is invalid.
|
|
109
|
+
*/
|
|
110
|
+
declare function parseGroq(query: string): ExprNode;
|
|
111
|
+
/**
|
|
112
|
+
* Check if a string is valid GROQ syntax.
|
|
113
|
+
* Returns true if valid, false if invalid.
|
|
114
|
+
*/
|
|
115
|
+
declare function isValidGroq(query: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Assert that a string is valid GROQ syntax.
|
|
118
|
+
* Throws an error with helpful context if invalid.
|
|
119
|
+
*
|
|
120
|
+
* Use this in tests to validate formatter output or generated queries.
|
|
121
|
+
*/
|
|
122
|
+
declare function assertValidGroq(query: string, context?: string): void;
|
|
123
|
+
|
|
124
|
+
export { type ExpectedError, type GroqParseError, type InvalidTestCase, RuleTester, type RuleTests, type ValidTestCase, assertValidGroq, isValidGroq, parseGroq };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { SchemaType, ExprNode } from 'groq-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Severity levels for lint findings
|
|
5
|
+
*/
|
|
6
|
+
type Severity = 'error' | 'warning' | 'info';
|
|
7
|
+
/**
|
|
8
|
+
* Category of the lint rule
|
|
9
|
+
*/
|
|
10
|
+
type Category = 'performance' | 'correctness' | 'style';
|
|
11
|
+
/**
|
|
12
|
+
* A location in the source query
|
|
13
|
+
*/
|
|
14
|
+
interface SourceLocation {
|
|
15
|
+
/** 1-based line number */
|
|
16
|
+
line: number;
|
|
17
|
+
/** 1-based column number */
|
|
18
|
+
column: number;
|
|
19
|
+
/** 0-based character offset */
|
|
20
|
+
offset: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A span in the source query
|
|
24
|
+
*/
|
|
25
|
+
interface SourceSpan {
|
|
26
|
+
start: SourceLocation;
|
|
27
|
+
end: SourceLocation;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A suggested fix for a finding
|
|
31
|
+
*/
|
|
32
|
+
interface Suggestion {
|
|
33
|
+
/** Description of what the suggestion does */
|
|
34
|
+
description: string;
|
|
35
|
+
/** The replacement text */
|
|
36
|
+
replacement: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A lint finding/diagnostic
|
|
40
|
+
*/
|
|
41
|
+
interface Finding {
|
|
42
|
+
/** Rule ID that produced this finding */
|
|
43
|
+
ruleId: string;
|
|
44
|
+
/** Human-readable message */
|
|
45
|
+
message: string;
|
|
46
|
+
/** Severity level */
|
|
47
|
+
severity: Severity;
|
|
48
|
+
/** Location in source (optional - some rules are query-wide) */
|
|
49
|
+
span?: SourceSpan;
|
|
50
|
+
/** Additional help text */
|
|
51
|
+
help?: string;
|
|
52
|
+
/** Suggested fixes */
|
|
53
|
+
suggestions?: Suggestion[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Context provided to rules during checking
|
|
57
|
+
*/
|
|
58
|
+
interface RuleContext {
|
|
59
|
+
/** The raw query string */
|
|
60
|
+
query: string;
|
|
61
|
+
/** Length of the query in bytes */
|
|
62
|
+
queryLength: number;
|
|
63
|
+
/** Report a finding */
|
|
64
|
+
report: (finding: Omit<Finding, 'ruleId'>) => void;
|
|
65
|
+
/** Schema for schema-aware rules (optional) */
|
|
66
|
+
schema?: SchemaType;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A lint rule definition
|
|
70
|
+
*/
|
|
71
|
+
interface Rule {
|
|
72
|
+
/** Unique identifier (kebab-case) */
|
|
73
|
+
id: string;
|
|
74
|
+
/** Human-readable name */
|
|
75
|
+
name: string;
|
|
76
|
+
/** Description of what the rule checks */
|
|
77
|
+
description: string;
|
|
78
|
+
/** Default severity */
|
|
79
|
+
severity: Severity;
|
|
80
|
+
/** Rule category */
|
|
81
|
+
category: Category;
|
|
82
|
+
/** Rule IDs that this rule supersedes */
|
|
83
|
+
supersedes?: string[];
|
|
84
|
+
/** Whether this rule requires a schema to function */
|
|
85
|
+
requiresSchema?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Check the AST for violations
|
|
88
|
+
* @param ast - The parsed GROQ AST
|
|
89
|
+
* @param context - Context with query info and report function
|
|
90
|
+
*/
|
|
91
|
+
check: (ast: ExprNode, context: RuleContext) => void;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Configuration for a rule
|
|
95
|
+
*/
|
|
96
|
+
interface RuleConfig {
|
|
97
|
+
/** Whether the rule is enabled */
|
|
98
|
+
enabled?: boolean;
|
|
99
|
+
/** Override severity */
|
|
100
|
+
severity?: Severity;
|
|
101
|
+
/** Rule-specific options */
|
|
102
|
+
options?: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Linter configuration
|
|
106
|
+
*/
|
|
107
|
+
interface LinterConfig {
|
|
108
|
+
/** Rule configurations keyed by rule ID */
|
|
109
|
+
rules?: Record<string, RuleConfig | boolean>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type { Category as C, Finding as F, LinterConfig as L, RuleContext as R, Severity as S, SourceLocation as a, SourceSpan as b, Suggestion as c, Rule as d, RuleConfig as e };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/lint-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Shared types and utilities for Sanity Lint",
|
|
5
5
|
"author": "Sanity.io <hello@sanity.io>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,14 +18,16 @@
|
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
|
-
"import": "./dist/index.js"
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
22
23
|
},
|
|
23
24
|
"./testing": {
|
|
24
25
|
"types": "./dist/testing.d.ts",
|
|
25
|
-
"import": "./dist/testing.js"
|
|
26
|
+
"import": "./dist/testing.js",
|
|
27
|
+
"require": "./dist/testing.cjs"
|
|
26
28
|
}
|
|
27
29
|
},
|
|
28
|
-
"main": "./dist/index.
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
29
31
|
"types": "./dist/index.d.ts",
|
|
30
32
|
"files": [
|
|
31
33
|
"dist"
|
|
@@ -38,7 +40,7 @@
|
|
|
38
40
|
"typescript": "^5.7.2"
|
|
39
41
|
},
|
|
40
42
|
"peerDependencies": {
|
|
41
|
-
"vitest": "
|
|
43
|
+
"vitest": ">=2.0.0"
|
|
42
44
|
},
|
|
43
45
|
"peerDependenciesMeta": {
|
|
44
46
|
"vitest": {
|