@jesscss/diagnostics-core 2.0.0-alpha.11

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Matthew Dean
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ import type { CssCstNode } from '@jesscss/css-parser';
2
+ export type CstIndexEntry = {
3
+ node: CssCstNode;
4
+ start: number;
5
+ end: number;
6
+ };
7
+ export type CstIndex = {
8
+ nodes: CstIndexEntry[];
9
+ spanOf(node: CssCstNode): {
10
+ start: number;
11
+ end: number;
12
+ } | undefined;
13
+ findNodeAtOffset(offset: number): CssCstNode | null;
14
+ };
15
+ export declare function buildCstIndex(root: CssCstNode): CstIndex;
package/lib/index.cjs ADDED
@@ -0,0 +1,503 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _jesscss_css_parser = require("@jesscss/css-parser");
3
+ let _jesscss_jess_parser_cst = require("@jesscss/jess-parser/cst");
4
+ let _jesscss_less_parser_cst = require("@jesscss/less-parser/cst");
5
+ let _jesscss_scss_parser_cst = require("@jesscss/scss-parser/cst");
6
+ let _jesscss_core_ast = require("@jesscss/core/ast");
7
+ //#region src/metadata.ts
8
+ const require$1 = (0, require("node:module").createRequire)(require("url").pathToFileURL(__filename).href);
9
+ const webCssData = require$1("@vscode/web-custom-data/data/browsers.css-data.json");
10
+ const knownCssProperties = require$1("known-css-properties");
11
+ function ownValue(value, key) {
12
+ if (typeof value !== "object" || value === null) return;
13
+ return Object.getOwnPropertyDescriptor(value, key)?.value;
14
+ }
15
+ function arrayField(value, key) {
16
+ const field = ownValue(value, key);
17
+ return Array.isArray(field) ? field : [];
18
+ }
19
+ function stringField(value, key) {
20
+ const field = ownValue(value, key);
21
+ return typeof field === "string" ? field : void 0;
22
+ }
23
+ const cssProperties = arrayField(knownCssProperties, "all").filter((value) => typeof value === "string");
24
+ const CSS_PROPERTY_SET = new Set(cssProperties.map((property) => property.toLowerCase()));
25
+ const WEB_PROPERTY_SET = new Set(arrayField(webCssData, "properties").map((property) => stringField(property, "name")?.toLowerCase()).filter((name) => typeof name === "string" && name.length > 0));
26
+ const AT_RULE_SET = new Set(arrayField(webCssData, "atDirectives").map((rule) => stringField(rule, "name")?.toLowerCase()).filter((name) => typeof name === "string" && name.length > 0));
27
+ const defaultCssDiagnosticMetadata = {
28
+ isKnownProperty(name) {
29
+ const lower = name.toLowerCase();
30
+ return CSS_PROPERTY_SET.has(lower) || WEB_PROPERTY_SET.has(lower);
31
+ },
32
+ isKnownAtRule(name) {
33
+ const lower = name.startsWith("@") ? name.toLowerCase() : `@${name.toLowerCase()}`;
34
+ return AT_RULE_SET.has(lower);
35
+ }
36
+ };
37
+ //#endregion
38
+ //#region src/tolerant-cst.ts
39
+ const LINT_CODES = {
40
+ emptyRules: "lint/empty-rules",
41
+ unknownProperties: "lint/unknown-property",
42
+ unknownAtRules: "lint/unknown-at-rule",
43
+ duplicateProperties: "lint/duplicate-property",
44
+ hexColorLength: "lint/hex-color-length",
45
+ zeroUnits: "lint/zero-units",
46
+ unsupportedSassForm: "unsupported/sass-form"
47
+ };
48
+ const LENGTH_UNITS = new Set([
49
+ "px",
50
+ "em",
51
+ "rem",
52
+ "ex",
53
+ "ch",
54
+ "cap",
55
+ "ic",
56
+ "lh",
57
+ "rlh",
58
+ "vw",
59
+ "vh",
60
+ "vi",
61
+ "vb",
62
+ "vmin",
63
+ "vmax",
64
+ "cm",
65
+ "mm",
66
+ "q",
67
+ "in",
68
+ "pt",
69
+ "pc"
70
+ ]);
71
+ const DIALECT_AT_RULES = {
72
+ css: /* @__PURE__ */ new Set(),
73
+ less: new Set(["plugin"]),
74
+ scss: new Set([
75
+ "mixin",
76
+ "include",
77
+ "function",
78
+ "return",
79
+ "if",
80
+ "else",
81
+ "each",
82
+ "for",
83
+ "while",
84
+ "use",
85
+ "forward",
86
+ "content",
87
+ "extend",
88
+ "at-root",
89
+ "debug",
90
+ "warn",
91
+ "error"
92
+ ]),
93
+ jess: new Set([
94
+ "mixin",
95
+ "include",
96
+ "function",
97
+ "return",
98
+ "if",
99
+ "else",
100
+ "each",
101
+ "for",
102
+ "while",
103
+ "use",
104
+ "forward",
105
+ "from",
106
+ "compose",
107
+ "content",
108
+ "extend",
109
+ "at-root",
110
+ "debug",
111
+ "warn",
112
+ "error"
113
+ ])
114
+ };
115
+ const RULESET_TYPES = new Set([
116
+ "Ruleset",
117
+ "DirectScssRule",
118
+ "DirectJessRule"
119
+ ]);
120
+ const ATRULE_TYPES = new Set([
121
+ "AtRuleBlock",
122
+ "AtRuleStatement",
123
+ "UnknownAtRuleBlock",
124
+ "QueryAtRuleBlock",
125
+ "OpaqueAtRuleBlock"
126
+ ]);
127
+ const DECLARATION_TYPES = new Set([
128
+ "Declaration",
129
+ "DirectScssDeclaration",
130
+ "DirectJessDeclaration"
131
+ ]);
132
+ const DIMENSION_TYPES = new Set([
133
+ "Dimension",
134
+ "DirectScssDimension",
135
+ "DirectJessDimension"
136
+ ]);
137
+ const FORWARD_AS_PREFIX = /\bas\s+\S+-\*/;
138
+ const FORWARD_VISIBILITY = /\b(show|hide)\b/;
139
+ function isCstNode$1(c) {
140
+ return c._tag === "node";
141
+ }
142
+ function isAstRecord(value) {
143
+ return typeof value === "object" && value !== null;
144
+ }
145
+ function astChildrenOf(value, key) {
146
+ if (!isAstRecord(value)) return [];
147
+ const child = value[key];
148
+ return Array.isArray(child) ? child : [];
149
+ }
150
+ function forwardPreludeOf(node, nodeStart, src) {
151
+ let afterPath = false;
152
+ for (const child of node.children) {
153
+ if (isCstNode$1(child)) {
154
+ if (child.grammarType === "Quoted") afterPath = true;
155
+ continue;
156
+ }
157
+ if (!afterPath) continue;
158
+ const normalized = src.slice(nodeStart + Number(child.span.start), nodeStart + Number(child.span.end)).replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n\r]*/g, " ").replace(/\s+/g, " ").trim();
159
+ if (normalized === ";" || normalized.toLowerCase() === "with") continue;
160
+ return normalized;
161
+ }
162
+ return null;
163
+ }
164
+ function propNameOf(slice) {
165
+ const colon = slice.indexOf(":");
166
+ return (colon >= 0 ? slice.slice(0, colon) : slice).trim();
167
+ }
168
+ function absoluteStart(base, node) {
169
+ return base + Number(node.span.start);
170
+ }
171
+ function absoluteEnd(base, node) {
172
+ return base + Number(node.span.end);
173
+ }
174
+ function isWhitespaceOnly(source, start, end) {
175
+ for (let i = start; i < end; i++) {
176
+ const code = source.charCodeAt(i);
177
+ if (code !== 9 && code !== 10 && code !== 12 && code !== 13 && code !== 32) return false;
178
+ }
179
+ return true;
180
+ }
181
+ function atRuleNameEnd(source, start, end) {
182
+ let i = start + 1;
183
+ while (i < end) {
184
+ const code = source.charCodeAt(i);
185
+ if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 45 || code === 95)) break;
186
+ i++;
187
+ }
188
+ return i;
189
+ }
190
+ function lineCommentStartBetween(source, start, end) {
191
+ for (let i = start; i < end - 1; i++) if (source.charCodeAt(i) === 47 && source.charCodeAt(i + 1) === 47) return i;
192
+ return -1;
193
+ }
194
+ function declarationEnd(source, start, blockEnd) {
195
+ const semi = source.indexOf(";", start);
196
+ if (semi < 0 || semi > blockEnd) return blockEnd;
197
+ return semi + 1;
198
+ }
199
+ function locateDeclaration(source, name, cursor, blockEnd) {
200
+ let search = cursor;
201
+ while (search < blockEnd) {
202
+ const nameStart = source.indexOf(name, search);
203
+ if (nameStart < 0 || nameStart >= blockEnd) return null;
204
+ const colon = source.indexOf(":", nameStart + name.length);
205
+ if (colon < 0 || colon >= blockEnd) return null;
206
+ const lineComment = lineCommentStartBetween(source, nameStart + name.length, colon);
207
+ if (lineComment < 0) return {
208
+ nameStart,
209
+ declarationEnd: declarationEnd(source, colon + 1, blockEnd),
210
+ valueStart: colon + 1
211
+ };
212
+ search = lineComment + 2;
213
+ }
214
+ return null;
215
+ }
216
+ function findValueSource(source, src, start, end) {
217
+ if (src.length === 0) return;
218
+ const valueStart = source.indexOf(src, start);
219
+ if (valueStart < 0 || valueStart >= end) return;
220
+ return {
221
+ start: valueStart,
222
+ end: valueStart + src.length
223
+ };
224
+ }
225
+ function blankStrings(value) {
226
+ return value.replace(/"[^"]*"|'[^']*'/g, (m) => " ".repeat(m.length));
227
+ }
228
+ function blankStringsAndComments(value) {
229
+ return value.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n\r]*|"[^"]*"|'[^']*'/g, (m) => " ".repeat(m.length));
230
+ }
231
+ function isDeclarationValueContext(source, offset) {
232
+ const lastBlockOpen = source.lastIndexOf("{", offset);
233
+ const lastBlockClose = source.lastIndexOf("}", offset);
234
+ if (lastBlockOpen < 0 || lastBlockClose > lastBlockOpen) return false;
235
+ const lastStatement = Math.max(lastBlockOpen, source.lastIndexOf(";", offset));
236
+ return source.lastIndexOf(":", offset) > lastStatement;
237
+ }
238
+ function diagnostic(code, defaultSeverity, message, start, end, filePath) {
239
+ return {
240
+ code,
241
+ phase: code.startsWith("parse/") ? "parse" : "lint",
242
+ source: "jess",
243
+ message,
244
+ reason: "",
245
+ fix: "",
246
+ defaultSeverity,
247
+ filePath,
248
+ start,
249
+ end: Math.max(start, end)
250
+ };
251
+ }
252
+ function metadataWithDefaults(metadata) {
253
+ return {
254
+ isKnownProperty(name) {
255
+ return metadata?.isKnownProperty?.(name) ?? defaultCssDiagnosticMetadata.isKnownProperty(name);
256
+ },
257
+ isKnownAtRule(name) {
258
+ return metadata?.isKnownAtRule?.(name) ?? defaultCssDiagnosticMetadata.isKnownAtRule(name);
259
+ }
260
+ };
261
+ }
262
+ function parseDocForLanguage(source, language) {
263
+ if (language === "less") return (0, _jesscss_less_parser_cst.parseLessDoc)(source);
264
+ if (language === "scss") return (0, _jesscss_scss_parser_cst.parseScssDoc)(source);
265
+ if (language === "jess") return (0, _jesscss_jess_parser_cst.parseJessDoc)(source);
266
+ return (0, _jesscss_css_parser.parseCssDoc)(source);
267
+ }
268
+ function parseDiagnosticsForDoc(doc, filePath) {
269
+ const diagnostics = [];
270
+ for (const error of doc.errors) diagnostics.push(diagnostic("parse/syntax-error", "error", "Unexpected syntax", Number(error.span.start), Number(error.span.end), filePath));
271
+ if (doc.unconsumedFrom !== null) diagnostics.push(diagnostic("parse/syntax-error", "error", "Unexpected input", doc.unconsumedFrom, doc.unconsumedFrom + 1, filePath));
272
+ return diagnostics;
273
+ }
274
+ function cstLintDiagnostics(root, source, language, metadata, filePath, tolerantSourceScan = true) {
275
+ const out = [];
276
+ const emitted = /* @__PURE__ */ new Set();
277
+ const cssData = metadataWithDefaults(metadata);
278
+ const dialectAtRules = DIALECT_AT_RULES[language];
279
+ const push = (code, severity, message, start, end) => {
280
+ const key = `${code}:${start}:${Math.max(start, end)}`;
281
+ if (emitted.has(key)) return;
282
+ emitted.add(key);
283
+ out.push(diagnostic(code, severity, message, start, end, filePath));
284
+ };
285
+ const visit = (node, base) => {
286
+ const start = absoluteStart(base, node);
287
+ const end = absoluteEnd(base, node);
288
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return;
289
+ const gt = node.grammarType;
290
+ if (RULESET_TYPES.has(gt)) {
291
+ const open = source.indexOf("{", start);
292
+ const close = source.lastIndexOf("}", end - 1);
293
+ if (open >= start && close > open && isWhitespaceOnly(source, open + 1, close)) push(LINT_CODES.emptyRules, "warning", "Do not use empty rulesets", start, end);
294
+ }
295
+ if (gt === "ScssAtRootFilter") push(LINT_CODES.unsupportedSassForm, "warning", "@at-root prelude/filter forms are not yet supported in Jess. Write the hoisted rules directly instead.", start, end);
296
+ if (gt === "ScssForward") {
297
+ const prelude = forwardPreludeOf(node, start, source);
298
+ if (prelude !== null) {
299
+ if (FORWARD_AS_PREFIX.test(prelude)) push(LINT_CODES.unsupportedSassForm, "warning", "@forward with \"as <prefix>-*\" prefixing is not supported in Jess and will never be. Use explicit namespacing instead.", start, end);
300
+ if (FORWARD_VISIBILITY.test(prelude)) push(LINT_CODES.unsupportedSassForm, "warning", "@forward with \"show\"/\"hide\" lists is not supported in Jess and will never be. Visibility control belongs to the module itself.", start, end);
301
+ }
302
+ }
303
+ if (ATRULE_TYPES.has(gt)) {
304
+ if (source.charCodeAt(start) === 64) {
305
+ const nameEnd = atRuleNameEnd(source, start, end);
306
+ if (nameEnd > start + 1) {
307
+ const rawName = source.slice(start + 1, nameEnd);
308
+ const name = rawName.toLowerCase();
309
+ if (!cssData.isKnownAtRule(name) && !dialectAtRules.has(name)) push(LINT_CODES.unknownAtRules, "warning", `Unknown at-rule @${rawName}`, start, nameEnd);
310
+ }
311
+ }
312
+ }
313
+ if (DIMENSION_TYPES.has(gt)) {
314
+ const slice = source.slice(start, end).trim();
315
+ const m = /^([+-]?(?:\d+\.?\d*|\.\d+))([a-z%]+)$/i.exec(slice);
316
+ if (m && Number(m[1]) === 0 && LENGTH_UNITS.has(m[2].toLowerCase())) push(LINT_CODES.zeroUnits, "hint", `The unit "${m[2]}" is unnecessary for a zero value`, start, end);
317
+ }
318
+ if (DECLARATION_TYPES.has(gt)) {
319
+ const slice = source.slice(start, end);
320
+ const colon = slice.indexOf(":");
321
+ const name = propNameOf(slice);
322
+ if (name.length > 0) {
323
+ const lower = name.toLowerCase();
324
+ if (!(lower.startsWith("--") || lower.startsWith("-") || lower.startsWith("$") || lower.startsWith("@") || lower.includes("#{") || lower.includes("@{") || lower.includes("${")) && !cssData.isKnownProperty(lower)) {
325
+ const nameStart = start + slice.indexOf(name);
326
+ push(LINT_CODES.unknownProperties, "warning", `Unknown property: '${name}'`, nameStart, nameStart + name.length);
327
+ }
328
+ }
329
+ if (colon >= 0) {
330
+ const valueStart = colon + 1;
331
+ const value = blankStrings(slice.slice(valueStart));
332
+ const hexRe = /#([0-9a-fA-F]+)/g;
333
+ let hm;
334
+ while ((hm = hexRe.exec(value)) !== null) {
335
+ const digits = hm[1].length;
336
+ if (digits !== 3 && digits !== 4 && digits !== 6 && digits !== 8) {
337
+ const hexStart = start + valueStart + hm.index;
338
+ push(LINT_CODES.hexColorLength, "error", `Hex color '${hm[0]}' does not have 3, 4, 6 or 8 digits`, hexStart, hexStart + hm[0].length);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ let seenProps;
344
+ for (const child of node.children) {
345
+ if (!isCstNode$1(child)) continue;
346
+ if (DECLARATION_TYPES.has(child.grammarType)) {
347
+ const childStart = absoluteStart(start, child);
348
+ const childEnd = absoluteEnd(start, child);
349
+ const name = propNameOf(source.slice(childStart, childEnd));
350
+ if (name.length > 0 && !name.includes("#{") && !name.includes("@{") && !name.includes("${")) {
351
+ const key = name.toLowerCase();
352
+ seenProps ??= /* @__PURE__ */ new Map();
353
+ if (seenProps.has(key)) push(LINT_CODES.duplicateProperties, "warning", `Duplicate property '${name}'`, childStart, childEnd);
354
+ else seenProps.set(key, true);
355
+ }
356
+ }
357
+ visit(child, start);
358
+ }
359
+ };
360
+ visit(root, 0);
361
+ if (tolerantSourceScan) {
362
+ const sourceForHexScan = blankStringsAndComments(source);
363
+ const sourceHexRe = /#([0-9a-fA-F]+)/g;
364
+ let match;
365
+ while ((match = sourceHexRe.exec(sourceForHexScan)) !== null) {
366
+ const digits = match[1].length;
367
+ if (digits !== 3 && digits !== 4 && digits !== 6 && digits !== 8 && isDeclarationValueContext(sourceForHexScan, match.index)) push(LINT_CODES.hexColorLength, "error", `Hex color '${match[0]}' does not have 3, 4, 6 or 8 digits`, match.index, match.index + match[0].length);
368
+ }
369
+ }
370
+ return out;
371
+ }
372
+ function visitValueDimensions(value, source, start, end, visit) {
373
+ if (Array.isArray(value)) {
374
+ for (const child of value) visitValueDimensions(child, source, start, end, visit);
375
+ return;
376
+ }
377
+ if (!isAstRecord(value)) return;
378
+ if (value.type === "Dimension" && value.number === 0 && typeof value.unit === "string" && typeof value.src === "string") visit(value.src, value.unit, findValueSource(source, value.src, start, end));
379
+ for (const child of Object.values(value)) if (isAstRecord(child) || Array.isArray(child)) visitValueDimensions(child, source, start, end, visit);
380
+ }
381
+ function cssAstLintDiagnostics(source, metadata, filePath) {
382
+ let root;
383
+ try {
384
+ root = (0, _jesscss_css_parser.parse)(source);
385
+ } catch {
386
+ return null;
387
+ }
388
+ const out = [];
389
+ const cssData = metadataWithDefaults(metadata);
390
+ const push = (code, severity, message, start, end) => {
391
+ out.push(diagnostic(code, severity, message, start, end, filePath));
392
+ };
393
+ const visitBody = (children, bodySpan) => {
394
+ const seenProps = /* @__PURE__ */ new Map();
395
+ let cursor = bodySpan?.start ?? 0;
396
+ const bodyEnd = bodySpan?.end ?? source.length;
397
+ for (const child of children) {
398
+ if (!isAstRecord(child)) continue;
399
+ if (child.type === "Declaration" && typeof child.name === "string") {
400
+ const located = locateDeclaration(source, child.name, cursor, bodyEnd);
401
+ const nameStart = located?.nameStart ?? cursor;
402
+ const declarationEndValue = located?.declarationEnd ?? nameStart + child.name.length;
403
+ const key = child.name.toLowerCase();
404
+ if (!(key.startsWith("--") || key.startsWith("-")) && !cssData.isKnownProperty(key)) push(LINT_CODES.unknownProperties, "warning", `Unknown property: '${child.name}'`, nameStart, nameStart + child.name.length);
405
+ if (seenProps.has(key)) push(LINT_CODES.duplicateProperties, "warning", `Duplicate property '${child.name}'`, nameStart, declarationEndValue);
406
+ else seenProps.set(key, true);
407
+ visitValueDimensions(child.value, source, located?.valueStart ?? nameStart, declarationEndValue, (src, unit, span) => {
408
+ if (LENGTH_UNITS.has(unit.toLowerCase())) push(LINT_CODES.zeroUnits, "hint", `The unit "${unit}" is unnecessary for a zero value`, span?.start ?? nameStart, span?.end ?? nameStart + src.length);
409
+ });
410
+ cursor = declarationEndValue;
411
+ } else {
412
+ visitAstNode(child);
413
+ const span = (0, _jesscss_core_ast.sourceSpanOf)(child) ?? (0, _jesscss_core_ast.bodySpanOf)(child);
414
+ if (span && span.end > cursor) cursor = span.end;
415
+ }
416
+ }
417
+ };
418
+ const visitAstNode = (node) => {
419
+ if (!isAstRecord(node)) return;
420
+ if (node.type === "Rule") {
421
+ const bodySpan = (0, _jesscss_core_ast.bodySpanOf)(node);
422
+ const body = astChildrenOf(node, "body");
423
+ if (body.length === 0 && bodySpan && isWhitespaceOnly(source, bodySpan.start, bodySpan.end)) {
424
+ const span = (0, _jesscss_core_ast.sourceSpanOf)(node) ?? bodySpan;
425
+ push(LINT_CODES.emptyRules, "warning", "Do not use empty rulesets", span.start, span.end);
426
+ }
427
+ visitBody(body, bodySpan);
428
+ return;
429
+ }
430
+ if ((node.type === "AtRuleStatement" || node.type === "AtRuleBlock") && typeof node.name === "string") {
431
+ const rawName = node.name.startsWith("@") ? node.name.slice(1) : node.name;
432
+ const lower = rawName.toLowerCase();
433
+ const span = (0, _jesscss_core_ast.sourceSpanOf)(node);
434
+ if (span && !cssData.isKnownAtRule(lower)) push(LINT_CODES.unknownAtRules, "warning", `Unknown at-rule @${rawName}`, span.start, span.start + rawName.length + 1);
435
+ if (node.type === "AtRuleBlock") visitBody(astChildrenOf(node, "body"), (0, _jesscss_core_ast.bodySpanOf)(node));
436
+ return;
437
+ }
438
+ visitBody(astChildrenOf(node, "children"), void 0);
439
+ };
440
+ visitAstNode(root);
441
+ return out;
442
+ }
443
+ function collectTolerantDiagnostics(input) {
444
+ if (input.language === "css") {
445
+ const cssAstDiagnostics = cssAstLintDiagnostics(input.source, input.metadata, input.filePath);
446
+ if (cssAstDiagnostics !== null) return { diagnostics: cssAstDiagnostics };
447
+ }
448
+ const doc = parseDocForLanguage(input.source, input.language);
449
+ const needsTolerantSourceScan = doc.errors.length > 0 || doc.unconsumedFrom !== null;
450
+ const lintDiagnostics = doc.tree ? cstLintDiagnostics(doc.tree, input.source, input.language, input.metadata, input.filePath, needsTolerantSourceScan) : [];
451
+ return { diagnostics: [...parseDiagnosticsForDoc(doc, input.filePath), ...lintDiagnostics] };
452
+ }
453
+ //#endregion
454
+ //#region src/cst-analysis.ts
455
+ function isCstNode(c) {
456
+ return c._tag === "node";
457
+ }
458
+ const INDEX_CACHE = /* @__PURE__ */ new WeakMap();
459
+ function buildCstIndex(root) {
460
+ const cached = INDEX_CACHE.get(root);
461
+ if (cached) return cached;
462
+ const out = [];
463
+ const abs = /* @__PURE__ */ new Map();
464
+ const walk = (node, base) => {
465
+ const s = base + Number(node.span.start);
466
+ const e = base + Number(node.span.end);
467
+ abs.set(node, [s, e]);
468
+ if (Number.isFinite(s) && Number.isFinite(e) && e >= s) out.push({
469
+ node,
470
+ start: s,
471
+ end: e
472
+ });
473
+ for (const child of node.children) if (isCstNode(child)) walk(child, s);
474
+ };
475
+ walk(root, 0);
476
+ out.sort((a, b) => a.start - b.start || a.end - b.end);
477
+ const index = {
478
+ nodes: out,
479
+ spanOf(node) {
480
+ const a = abs.get(node);
481
+ return a ? {
482
+ start: a[0],
483
+ end: a[1]
484
+ } : void 0;
485
+ },
486
+ findNodeAtOffset(offset) {
487
+ let best = null;
488
+ for (const entry of out) if (entry.start <= offset && offset <= entry.end) {
489
+ if (!best || entry.end - entry.start <= best.end - best.start) best = entry;
490
+ }
491
+ return best?.node ?? null;
492
+ }
493
+ };
494
+ INDEX_CACHE.set(root, index);
495
+ return index;
496
+ }
497
+ //#endregion
498
+ exports.LINT_CODES = LINT_CODES;
499
+ exports.buildCstIndex = buildCstIndex;
500
+ exports.collectTolerantDiagnostics = collectTolerantDiagnostics;
501
+ exports.cstLintDiagnostics = cstLintDiagnostics;
502
+ exports.defaultCssDiagnosticMetadata = defaultCssDiagnosticMetadata;
503
+ exports.parseDocForLanguage = parseDocForLanguage;
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { LINT_CODES, collectTolerantDiagnostics, cstLintDiagnostics, parseDocForLanguage } from './tolerant-cst.js';
2
+ export { buildCstIndex, type CstIndex, type CstIndexEntry } from './cst-analysis.js';
3
+ export { defaultCssDiagnosticMetadata } from './metadata.js';
4
+ export type { CollectDiagnosticsInput, CollectDiagnosticsResult, CssDiagnosticMetadata, DiagnosticSeverityName, JessLanguage, SourceDiagnostic } from './types.js';
package/lib/index.js ADDED
@@ -0,0 +1,498 @@
1
+ import { createRequire } from "node:module";
2
+ import { parse, parseCssDoc } from "@jesscss/css-parser";
3
+ import { parseJessDoc } from "@jesscss/jess-parser/cst";
4
+ import { parseLessDoc } from "@jesscss/less-parser/cst";
5
+ import { parseScssDoc } from "@jesscss/scss-parser/cst";
6
+ import { bodySpanOf, sourceSpanOf } from "@jesscss/core/ast";
7
+ //#region src/metadata.ts
8
+ const require = createRequire(import.meta.url);
9
+ const webCssData = require("@vscode/web-custom-data/data/browsers.css-data.json");
10
+ const knownCssProperties = require("known-css-properties");
11
+ function ownValue(value, key) {
12
+ if (typeof value !== "object" || value === null) return;
13
+ return Object.getOwnPropertyDescriptor(value, key)?.value;
14
+ }
15
+ function arrayField(value, key) {
16
+ const field = ownValue(value, key);
17
+ return Array.isArray(field) ? field : [];
18
+ }
19
+ function stringField(value, key) {
20
+ const field = ownValue(value, key);
21
+ return typeof field === "string" ? field : void 0;
22
+ }
23
+ const cssProperties = arrayField(knownCssProperties, "all").filter((value) => typeof value === "string");
24
+ const CSS_PROPERTY_SET = new Set(cssProperties.map((property) => property.toLowerCase()));
25
+ const WEB_PROPERTY_SET = new Set(arrayField(webCssData, "properties").map((property) => stringField(property, "name")?.toLowerCase()).filter((name) => typeof name === "string" && name.length > 0));
26
+ const AT_RULE_SET = new Set(arrayField(webCssData, "atDirectives").map((rule) => stringField(rule, "name")?.toLowerCase()).filter((name) => typeof name === "string" && name.length > 0));
27
+ const defaultCssDiagnosticMetadata = {
28
+ isKnownProperty(name) {
29
+ const lower = name.toLowerCase();
30
+ return CSS_PROPERTY_SET.has(lower) || WEB_PROPERTY_SET.has(lower);
31
+ },
32
+ isKnownAtRule(name) {
33
+ const lower = name.startsWith("@") ? name.toLowerCase() : `@${name.toLowerCase()}`;
34
+ return AT_RULE_SET.has(lower);
35
+ }
36
+ };
37
+ //#endregion
38
+ //#region src/tolerant-cst.ts
39
+ const LINT_CODES = {
40
+ emptyRules: "lint/empty-rules",
41
+ unknownProperties: "lint/unknown-property",
42
+ unknownAtRules: "lint/unknown-at-rule",
43
+ duplicateProperties: "lint/duplicate-property",
44
+ hexColorLength: "lint/hex-color-length",
45
+ zeroUnits: "lint/zero-units",
46
+ unsupportedSassForm: "unsupported/sass-form"
47
+ };
48
+ const LENGTH_UNITS = new Set([
49
+ "px",
50
+ "em",
51
+ "rem",
52
+ "ex",
53
+ "ch",
54
+ "cap",
55
+ "ic",
56
+ "lh",
57
+ "rlh",
58
+ "vw",
59
+ "vh",
60
+ "vi",
61
+ "vb",
62
+ "vmin",
63
+ "vmax",
64
+ "cm",
65
+ "mm",
66
+ "q",
67
+ "in",
68
+ "pt",
69
+ "pc"
70
+ ]);
71
+ const DIALECT_AT_RULES = {
72
+ css: /* @__PURE__ */ new Set(),
73
+ less: new Set(["plugin"]),
74
+ scss: new Set([
75
+ "mixin",
76
+ "include",
77
+ "function",
78
+ "return",
79
+ "if",
80
+ "else",
81
+ "each",
82
+ "for",
83
+ "while",
84
+ "use",
85
+ "forward",
86
+ "content",
87
+ "extend",
88
+ "at-root",
89
+ "debug",
90
+ "warn",
91
+ "error"
92
+ ]),
93
+ jess: new Set([
94
+ "mixin",
95
+ "include",
96
+ "function",
97
+ "return",
98
+ "if",
99
+ "else",
100
+ "each",
101
+ "for",
102
+ "while",
103
+ "use",
104
+ "forward",
105
+ "from",
106
+ "compose",
107
+ "content",
108
+ "extend",
109
+ "at-root",
110
+ "debug",
111
+ "warn",
112
+ "error"
113
+ ])
114
+ };
115
+ const RULESET_TYPES = new Set([
116
+ "Ruleset",
117
+ "DirectScssRule",
118
+ "DirectJessRule"
119
+ ]);
120
+ const ATRULE_TYPES = new Set([
121
+ "AtRuleBlock",
122
+ "AtRuleStatement",
123
+ "UnknownAtRuleBlock",
124
+ "QueryAtRuleBlock",
125
+ "OpaqueAtRuleBlock"
126
+ ]);
127
+ const DECLARATION_TYPES = new Set([
128
+ "Declaration",
129
+ "DirectScssDeclaration",
130
+ "DirectJessDeclaration"
131
+ ]);
132
+ const DIMENSION_TYPES = new Set([
133
+ "Dimension",
134
+ "DirectScssDimension",
135
+ "DirectJessDimension"
136
+ ]);
137
+ const FORWARD_AS_PREFIX = /\bas\s+\S+-\*/;
138
+ const FORWARD_VISIBILITY = /\b(show|hide)\b/;
139
+ function isCstNode$1(c) {
140
+ return c._tag === "node";
141
+ }
142
+ function isAstRecord(value) {
143
+ return typeof value === "object" && value !== null;
144
+ }
145
+ function astChildrenOf(value, key) {
146
+ if (!isAstRecord(value)) return [];
147
+ const child = value[key];
148
+ return Array.isArray(child) ? child : [];
149
+ }
150
+ function forwardPreludeOf(node, nodeStart, src) {
151
+ let afterPath = false;
152
+ for (const child of node.children) {
153
+ if (isCstNode$1(child)) {
154
+ if (child.grammarType === "Quoted") afterPath = true;
155
+ continue;
156
+ }
157
+ if (!afterPath) continue;
158
+ const normalized = src.slice(nodeStart + Number(child.span.start), nodeStart + Number(child.span.end)).replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n\r]*/g, " ").replace(/\s+/g, " ").trim();
159
+ if (normalized === ";" || normalized.toLowerCase() === "with") continue;
160
+ return normalized;
161
+ }
162
+ return null;
163
+ }
164
+ function propNameOf(slice) {
165
+ const colon = slice.indexOf(":");
166
+ return (colon >= 0 ? slice.slice(0, colon) : slice).trim();
167
+ }
168
+ function absoluteStart(base, node) {
169
+ return base + Number(node.span.start);
170
+ }
171
+ function absoluteEnd(base, node) {
172
+ return base + Number(node.span.end);
173
+ }
174
+ function isWhitespaceOnly(source, start, end) {
175
+ for (let i = start; i < end; i++) {
176
+ const code = source.charCodeAt(i);
177
+ if (code !== 9 && code !== 10 && code !== 12 && code !== 13 && code !== 32) return false;
178
+ }
179
+ return true;
180
+ }
181
+ function atRuleNameEnd(source, start, end) {
182
+ let i = start + 1;
183
+ while (i < end) {
184
+ const code = source.charCodeAt(i);
185
+ if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 45 || code === 95)) break;
186
+ i++;
187
+ }
188
+ return i;
189
+ }
190
+ function lineCommentStartBetween(source, start, end) {
191
+ for (let i = start; i < end - 1; i++) if (source.charCodeAt(i) === 47 && source.charCodeAt(i + 1) === 47) return i;
192
+ return -1;
193
+ }
194
+ function declarationEnd(source, start, blockEnd) {
195
+ const semi = source.indexOf(";", start);
196
+ if (semi < 0 || semi > blockEnd) return blockEnd;
197
+ return semi + 1;
198
+ }
199
+ function locateDeclaration(source, name, cursor, blockEnd) {
200
+ let search = cursor;
201
+ while (search < blockEnd) {
202
+ const nameStart = source.indexOf(name, search);
203
+ if (nameStart < 0 || nameStart >= blockEnd) return null;
204
+ const colon = source.indexOf(":", nameStart + name.length);
205
+ if (colon < 0 || colon >= blockEnd) return null;
206
+ const lineComment = lineCommentStartBetween(source, nameStart + name.length, colon);
207
+ if (lineComment < 0) return {
208
+ nameStart,
209
+ declarationEnd: declarationEnd(source, colon + 1, blockEnd),
210
+ valueStart: colon + 1
211
+ };
212
+ search = lineComment + 2;
213
+ }
214
+ return null;
215
+ }
216
+ function findValueSource(source, src, start, end) {
217
+ if (src.length === 0) return;
218
+ const valueStart = source.indexOf(src, start);
219
+ if (valueStart < 0 || valueStart >= end) return;
220
+ return {
221
+ start: valueStart,
222
+ end: valueStart + src.length
223
+ };
224
+ }
225
+ function blankStrings(value) {
226
+ return value.replace(/"[^"]*"|'[^']*'/g, (m) => " ".repeat(m.length));
227
+ }
228
+ function blankStringsAndComments(value) {
229
+ return value.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n\r]*|"[^"]*"|'[^']*'/g, (m) => " ".repeat(m.length));
230
+ }
231
+ function isDeclarationValueContext(source, offset) {
232
+ const lastBlockOpen = source.lastIndexOf("{", offset);
233
+ const lastBlockClose = source.lastIndexOf("}", offset);
234
+ if (lastBlockOpen < 0 || lastBlockClose > lastBlockOpen) return false;
235
+ const lastStatement = Math.max(lastBlockOpen, source.lastIndexOf(";", offset));
236
+ return source.lastIndexOf(":", offset) > lastStatement;
237
+ }
238
+ function diagnostic(code, defaultSeverity, message, start, end, filePath) {
239
+ return {
240
+ code,
241
+ phase: code.startsWith("parse/") ? "parse" : "lint",
242
+ source: "jess",
243
+ message,
244
+ reason: "",
245
+ fix: "",
246
+ defaultSeverity,
247
+ filePath,
248
+ start,
249
+ end: Math.max(start, end)
250
+ };
251
+ }
252
+ function metadataWithDefaults(metadata) {
253
+ return {
254
+ isKnownProperty(name) {
255
+ return metadata?.isKnownProperty?.(name) ?? defaultCssDiagnosticMetadata.isKnownProperty(name);
256
+ },
257
+ isKnownAtRule(name) {
258
+ return metadata?.isKnownAtRule?.(name) ?? defaultCssDiagnosticMetadata.isKnownAtRule(name);
259
+ }
260
+ };
261
+ }
262
+ function parseDocForLanguage(source, language) {
263
+ if (language === "less") return parseLessDoc(source);
264
+ if (language === "scss") return parseScssDoc(source);
265
+ if (language === "jess") return parseJessDoc(source);
266
+ return parseCssDoc(source);
267
+ }
268
+ function parseDiagnosticsForDoc(doc, filePath) {
269
+ const diagnostics = [];
270
+ for (const error of doc.errors) diagnostics.push(diagnostic("parse/syntax-error", "error", "Unexpected syntax", Number(error.span.start), Number(error.span.end), filePath));
271
+ if (doc.unconsumedFrom !== null) diagnostics.push(diagnostic("parse/syntax-error", "error", "Unexpected input", doc.unconsumedFrom, doc.unconsumedFrom + 1, filePath));
272
+ return diagnostics;
273
+ }
274
+ function cstLintDiagnostics(root, source, language, metadata, filePath, tolerantSourceScan = true) {
275
+ const out = [];
276
+ const emitted = /* @__PURE__ */ new Set();
277
+ const cssData = metadataWithDefaults(metadata);
278
+ const dialectAtRules = DIALECT_AT_RULES[language];
279
+ const push = (code, severity, message, start, end) => {
280
+ const key = `${code}:${start}:${Math.max(start, end)}`;
281
+ if (emitted.has(key)) return;
282
+ emitted.add(key);
283
+ out.push(diagnostic(code, severity, message, start, end, filePath));
284
+ };
285
+ const visit = (node, base) => {
286
+ const start = absoluteStart(base, node);
287
+ const end = absoluteEnd(base, node);
288
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return;
289
+ const gt = node.grammarType;
290
+ if (RULESET_TYPES.has(gt)) {
291
+ const open = source.indexOf("{", start);
292
+ const close = source.lastIndexOf("}", end - 1);
293
+ if (open >= start && close > open && isWhitespaceOnly(source, open + 1, close)) push(LINT_CODES.emptyRules, "warning", "Do not use empty rulesets", start, end);
294
+ }
295
+ if (gt === "ScssAtRootFilter") push(LINT_CODES.unsupportedSassForm, "warning", "@at-root prelude/filter forms are not yet supported in Jess. Write the hoisted rules directly instead.", start, end);
296
+ if (gt === "ScssForward") {
297
+ const prelude = forwardPreludeOf(node, start, source);
298
+ if (prelude !== null) {
299
+ if (FORWARD_AS_PREFIX.test(prelude)) push(LINT_CODES.unsupportedSassForm, "warning", "@forward with \"as <prefix>-*\" prefixing is not supported in Jess and will never be. Use explicit namespacing instead.", start, end);
300
+ if (FORWARD_VISIBILITY.test(prelude)) push(LINT_CODES.unsupportedSassForm, "warning", "@forward with \"show\"/\"hide\" lists is not supported in Jess and will never be. Visibility control belongs to the module itself.", start, end);
301
+ }
302
+ }
303
+ if (ATRULE_TYPES.has(gt)) {
304
+ if (source.charCodeAt(start) === 64) {
305
+ const nameEnd = atRuleNameEnd(source, start, end);
306
+ if (nameEnd > start + 1) {
307
+ const rawName = source.slice(start + 1, nameEnd);
308
+ const name = rawName.toLowerCase();
309
+ if (!cssData.isKnownAtRule(name) && !dialectAtRules.has(name)) push(LINT_CODES.unknownAtRules, "warning", `Unknown at-rule @${rawName}`, start, nameEnd);
310
+ }
311
+ }
312
+ }
313
+ if (DIMENSION_TYPES.has(gt)) {
314
+ const slice = source.slice(start, end).trim();
315
+ const m = /^([+-]?(?:\d+\.?\d*|\.\d+))([a-z%]+)$/i.exec(slice);
316
+ if (m && Number(m[1]) === 0 && LENGTH_UNITS.has(m[2].toLowerCase())) push(LINT_CODES.zeroUnits, "hint", `The unit "${m[2]}" is unnecessary for a zero value`, start, end);
317
+ }
318
+ if (DECLARATION_TYPES.has(gt)) {
319
+ const slice = source.slice(start, end);
320
+ const colon = slice.indexOf(":");
321
+ const name = propNameOf(slice);
322
+ if (name.length > 0) {
323
+ const lower = name.toLowerCase();
324
+ if (!(lower.startsWith("--") || lower.startsWith("-") || lower.startsWith("$") || lower.startsWith("@") || lower.includes("#{") || lower.includes("@{") || lower.includes("${")) && !cssData.isKnownProperty(lower)) {
325
+ const nameStart = start + slice.indexOf(name);
326
+ push(LINT_CODES.unknownProperties, "warning", `Unknown property: '${name}'`, nameStart, nameStart + name.length);
327
+ }
328
+ }
329
+ if (colon >= 0) {
330
+ const valueStart = colon + 1;
331
+ const value = blankStrings(slice.slice(valueStart));
332
+ const hexRe = /#([0-9a-fA-F]+)/g;
333
+ let hm;
334
+ while ((hm = hexRe.exec(value)) !== null) {
335
+ const digits = hm[1].length;
336
+ if (digits !== 3 && digits !== 4 && digits !== 6 && digits !== 8) {
337
+ const hexStart = start + valueStart + hm.index;
338
+ push(LINT_CODES.hexColorLength, "error", `Hex color '${hm[0]}' does not have 3, 4, 6 or 8 digits`, hexStart, hexStart + hm[0].length);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ let seenProps;
344
+ for (const child of node.children) {
345
+ if (!isCstNode$1(child)) continue;
346
+ if (DECLARATION_TYPES.has(child.grammarType)) {
347
+ const childStart = absoluteStart(start, child);
348
+ const childEnd = absoluteEnd(start, child);
349
+ const name = propNameOf(source.slice(childStart, childEnd));
350
+ if (name.length > 0 && !name.includes("#{") && !name.includes("@{") && !name.includes("${")) {
351
+ const key = name.toLowerCase();
352
+ seenProps ??= /* @__PURE__ */ new Map();
353
+ if (seenProps.has(key)) push(LINT_CODES.duplicateProperties, "warning", `Duplicate property '${name}'`, childStart, childEnd);
354
+ else seenProps.set(key, true);
355
+ }
356
+ }
357
+ visit(child, start);
358
+ }
359
+ };
360
+ visit(root, 0);
361
+ if (tolerantSourceScan) {
362
+ const sourceForHexScan = blankStringsAndComments(source);
363
+ const sourceHexRe = /#([0-9a-fA-F]+)/g;
364
+ let match;
365
+ while ((match = sourceHexRe.exec(sourceForHexScan)) !== null) {
366
+ const digits = match[1].length;
367
+ if (digits !== 3 && digits !== 4 && digits !== 6 && digits !== 8 && isDeclarationValueContext(sourceForHexScan, match.index)) push(LINT_CODES.hexColorLength, "error", `Hex color '${match[0]}' does not have 3, 4, 6 or 8 digits`, match.index, match.index + match[0].length);
368
+ }
369
+ }
370
+ return out;
371
+ }
372
+ function visitValueDimensions(value, source, start, end, visit) {
373
+ if (Array.isArray(value)) {
374
+ for (const child of value) visitValueDimensions(child, source, start, end, visit);
375
+ return;
376
+ }
377
+ if (!isAstRecord(value)) return;
378
+ if (value.type === "Dimension" && value.number === 0 && typeof value.unit === "string" && typeof value.src === "string") visit(value.src, value.unit, findValueSource(source, value.src, start, end));
379
+ for (const child of Object.values(value)) if (isAstRecord(child) || Array.isArray(child)) visitValueDimensions(child, source, start, end, visit);
380
+ }
381
+ function cssAstLintDiagnostics(source, metadata, filePath) {
382
+ let root;
383
+ try {
384
+ root = parse(source);
385
+ } catch {
386
+ return null;
387
+ }
388
+ const out = [];
389
+ const cssData = metadataWithDefaults(metadata);
390
+ const push = (code, severity, message, start, end) => {
391
+ out.push(diagnostic(code, severity, message, start, end, filePath));
392
+ };
393
+ const visitBody = (children, bodySpan) => {
394
+ const seenProps = /* @__PURE__ */ new Map();
395
+ let cursor = bodySpan?.start ?? 0;
396
+ const bodyEnd = bodySpan?.end ?? source.length;
397
+ for (const child of children) {
398
+ if (!isAstRecord(child)) continue;
399
+ if (child.type === "Declaration" && typeof child.name === "string") {
400
+ const located = locateDeclaration(source, child.name, cursor, bodyEnd);
401
+ const nameStart = located?.nameStart ?? cursor;
402
+ const declarationEndValue = located?.declarationEnd ?? nameStart + child.name.length;
403
+ const key = child.name.toLowerCase();
404
+ if (!(key.startsWith("--") || key.startsWith("-")) && !cssData.isKnownProperty(key)) push(LINT_CODES.unknownProperties, "warning", `Unknown property: '${child.name}'`, nameStart, nameStart + child.name.length);
405
+ if (seenProps.has(key)) push(LINT_CODES.duplicateProperties, "warning", `Duplicate property '${child.name}'`, nameStart, declarationEndValue);
406
+ else seenProps.set(key, true);
407
+ visitValueDimensions(child.value, source, located?.valueStart ?? nameStart, declarationEndValue, (src, unit, span) => {
408
+ if (LENGTH_UNITS.has(unit.toLowerCase())) push(LINT_CODES.zeroUnits, "hint", `The unit "${unit}" is unnecessary for a zero value`, span?.start ?? nameStart, span?.end ?? nameStart + src.length);
409
+ });
410
+ cursor = declarationEndValue;
411
+ } else {
412
+ visitAstNode(child);
413
+ const span = sourceSpanOf(child) ?? bodySpanOf(child);
414
+ if (span && span.end > cursor) cursor = span.end;
415
+ }
416
+ }
417
+ };
418
+ const visitAstNode = (node) => {
419
+ if (!isAstRecord(node)) return;
420
+ if (node.type === "Rule") {
421
+ const bodySpan = bodySpanOf(node);
422
+ const body = astChildrenOf(node, "body");
423
+ if (body.length === 0 && bodySpan && isWhitespaceOnly(source, bodySpan.start, bodySpan.end)) {
424
+ const span = sourceSpanOf(node) ?? bodySpan;
425
+ push(LINT_CODES.emptyRules, "warning", "Do not use empty rulesets", span.start, span.end);
426
+ }
427
+ visitBody(body, bodySpan);
428
+ return;
429
+ }
430
+ if ((node.type === "AtRuleStatement" || node.type === "AtRuleBlock") && typeof node.name === "string") {
431
+ const rawName = node.name.startsWith("@") ? node.name.slice(1) : node.name;
432
+ const lower = rawName.toLowerCase();
433
+ const span = sourceSpanOf(node);
434
+ if (span && !cssData.isKnownAtRule(lower)) push(LINT_CODES.unknownAtRules, "warning", `Unknown at-rule @${rawName}`, span.start, span.start + rawName.length + 1);
435
+ if (node.type === "AtRuleBlock") visitBody(astChildrenOf(node, "body"), bodySpanOf(node));
436
+ return;
437
+ }
438
+ visitBody(astChildrenOf(node, "children"), void 0);
439
+ };
440
+ visitAstNode(root);
441
+ return out;
442
+ }
443
+ function collectTolerantDiagnostics(input) {
444
+ if (input.language === "css") {
445
+ const cssAstDiagnostics = cssAstLintDiagnostics(input.source, input.metadata, input.filePath);
446
+ if (cssAstDiagnostics !== null) return { diagnostics: cssAstDiagnostics };
447
+ }
448
+ const doc = parseDocForLanguage(input.source, input.language);
449
+ const needsTolerantSourceScan = doc.errors.length > 0 || doc.unconsumedFrom !== null;
450
+ const lintDiagnostics = doc.tree ? cstLintDiagnostics(doc.tree, input.source, input.language, input.metadata, input.filePath, needsTolerantSourceScan) : [];
451
+ return { diagnostics: [...parseDiagnosticsForDoc(doc, input.filePath), ...lintDiagnostics] };
452
+ }
453
+ //#endregion
454
+ //#region src/cst-analysis.ts
455
+ function isCstNode(c) {
456
+ return c._tag === "node";
457
+ }
458
+ const INDEX_CACHE = /* @__PURE__ */ new WeakMap();
459
+ function buildCstIndex(root) {
460
+ const cached = INDEX_CACHE.get(root);
461
+ if (cached) return cached;
462
+ const out = [];
463
+ const abs = /* @__PURE__ */ new Map();
464
+ const walk = (node, base) => {
465
+ const s = base + Number(node.span.start);
466
+ const e = base + Number(node.span.end);
467
+ abs.set(node, [s, e]);
468
+ if (Number.isFinite(s) && Number.isFinite(e) && e >= s) out.push({
469
+ node,
470
+ start: s,
471
+ end: e
472
+ });
473
+ for (const child of node.children) if (isCstNode(child)) walk(child, s);
474
+ };
475
+ walk(root, 0);
476
+ out.sort((a, b) => a.start - b.start || a.end - b.end);
477
+ const index = {
478
+ nodes: out,
479
+ spanOf(node) {
480
+ const a = abs.get(node);
481
+ return a ? {
482
+ start: a[0],
483
+ end: a[1]
484
+ } : void 0;
485
+ },
486
+ findNodeAtOffset(offset) {
487
+ let best = null;
488
+ for (const entry of out) if (entry.start <= offset && offset <= entry.end) {
489
+ if (!best || entry.end - entry.start <= best.end - best.start) best = entry;
490
+ }
491
+ return best?.node ?? null;
492
+ }
493
+ };
494
+ INDEX_CACHE.set(root, index);
495
+ return index;
496
+ }
497
+ //#endregion
498
+ export { LINT_CODES, buildCstIndex, collectTolerantDiagnostics, cstLintDiagnostics, defaultCssDiagnosticMetadata, parseDocForLanguage };
@@ -0,0 +1,2 @@
1
+ import type { CssDiagnosticMetadata } from './types.js';
2
+ export declare const defaultCssDiagnosticMetadata: CssDiagnosticMetadata;
@@ -0,0 +1,15 @@
1
+ import { type CssCstNode, type ParseDoc } from '@jesscss/css-parser';
2
+ import type { CollectDiagnosticsInput, CollectDiagnosticsResult, CssDiagnosticMetadata, JessLanguage, SourceDiagnostic } from './types.js';
3
+ export declare const LINT_CODES: {
4
+ readonly emptyRules: "lint/empty-rules";
5
+ readonly unknownProperties: "lint/unknown-property";
6
+ readonly unknownAtRules: "lint/unknown-at-rule";
7
+ readonly duplicateProperties: "lint/duplicate-property";
8
+ readonly hexColorLength: "lint/hex-color-length";
9
+ readonly zeroUnits: "lint/zero-units";
10
+ readonly unsupportedSassForm: "unsupported/sass-form";
11
+ };
12
+ export declare function parseDocForLanguage(source: string, language: JessLanguage): ParseDoc<CssCstNode>;
13
+ export declare function parseDiagnosticsForDoc(doc: ParseDoc<CssCstNode>, filePath?: string): SourceDiagnostic[];
14
+ export declare function cstLintDiagnostics(root: CssCstNode, source: string, language: JessLanguage, metadata?: Partial<CssDiagnosticMetadata>, filePath?: string, tolerantSourceScan?: boolean): SourceDiagnostic[];
15
+ export declare function collectTolerantDiagnostics(input: CollectDiagnosticsInput): CollectDiagnosticsResult;
package/lib/types.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { Phase } from '@jesscss/core';
2
+ export type JessLanguage = 'css' | 'less' | 'scss' | 'jess';
3
+ export type DiagnosticSeverityName = 'error' | 'warning' | 'information' | 'hint';
4
+ export interface SourceDiagnostic {
5
+ readonly code: string;
6
+ readonly phase: Phase;
7
+ readonly source: 'jess';
8
+ readonly message: string;
9
+ readonly reason: string;
10
+ readonly fix: string;
11
+ readonly defaultSeverity: DiagnosticSeverityName;
12
+ readonly filePath?: string;
13
+ readonly start: number;
14
+ readonly end: number;
15
+ }
16
+ export interface CssDiagnosticMetadata {
17
+ isKnownProperty(name: string): boolean;
18
+ isKnownAtRule(name: string): boolean;
19
+ }
20
+ export interface CollectDiagnosticsInput {
21
+ readonly source: string;
22
+ readonly language: JessLanguage;
23
+ readonly filePath?: string;
24
+ readonly metadata?: Partial<CssDiagnosticMetadata>;
25
+ }
26
+ export interface CollectDiagnosticsResult {
27
+ readonly diagnostics: readonly SourceDiagnostic[];
28
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@jesscss/diagnostics-core",
3
+ "type": "module",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Shared Jess stylesheet diagnostics for editor and CLI surfaces",
8
+ "version": "2.0.0-alpha.11",
9
+ "engines": {
10
+ "node": "^20.19.0 || >=22.12.0"
11
+ },
12
+ "main": "lib/index.cjs",
13
+ "types": "lib/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./lib/index.d.ts",
17
+ "import": "./lib/index.js",
18
+ "require": "./lib/index.cjs"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "lib"
24
+ ],
25
+ "dependencies": {
26
+ "@vscode/web-custom-data": "^0.6.2",
27
+ "known-css-properties": "~0.37.0",
28
+ "@jesscss/core": "2.0.0-alpha.11",
29
+ "@jesscss/css-parser": "2.0.0-alpha.11",
30
+ "@jesscss/jess-parser": "2.0.0-alpha.11",
31
+ "@jesscss/less-parser": "2.0.0-alpha.11",
32
+ "@jesscss/scss-parser": "2.0.0-alpha.11"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.0.0",
36
+ "typescript": "~5.8.2",
37
+ "vitest": "^4.1.0"
38
+ },
39
+ "author": "Matthew Dean <matthew-dean@users.noreply.github.com>",
40
+ "license": "MIT",
41
+ "bugs": {
42
+ "url": "https://github.com/jesscss/jess/issues"
43
+ },
44
+ "homepage": "https://github.com/jesscss/jess#readme",
45
+ "module": "lib/index.js",
46
+ "scripts": {
47
+ "build": "pnpm compile",
48
+ "compile": "tsdown --tsconfig tsconfig.build.json --no-dts && tsc -p tsconfig.build.json --emitDeclarationOnly --noCheck",
49
+ "test": "vitest --watch=false",
50
+ "lint": "eslint '**/*.{js,ts}'"
51
+ }
52
+ }