Repository: stylelint/vscode-stylelint (ts)

Issue:
[Feature]: Emit diagnostics for ranges instead of single positions
### What problem does this feature solve?

Unlike other linter integrations, we only display diagnostics for a single position. This makes for a confusing and less useful experience for users.

### How does this feature solve the problem?

We should emit diagnostics for ranges that better describe the location of problems.

### What are some alternatives to this feature?

_No response_

### Code of Conduct

- [X] I agree to follow vscode-stylelint's Code of Conduct

Pull request description:
feat: Report ranges for diagnostics
Resolves #336

Reports ranges for diagnostics.

This branch is being used to test stylelint/stylelint#5725.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f9c7cb..c72009c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 - Language server can now be restarted with the "Restart Stylelint Server" command ([#339](https://github.com/stylelint/vscode-stylelint/pull/339)).
 - The warning shown for unsupported versions of Stylelint will now be shown in any workspace with a locally installed copy of Stylelint, regardless of the file type of the open file ([#340](https://github.com/stylelint/vscode-stylelint/pull/340)).
 - Language server can now be disposed with `server.dispose()` and is disposed when receiving the shutdown LSP notification ([#326](https://github.com/stylelint/vscode-stylelint/pull/326)).
+- Add support for range warnings ([#358](https://github.com/stylelint/vscode-stylelint/pull/358)).
 
 ### Changed
 
diff --git a/src/utils/stylelint/warning-to-diagnostic.ts b/src/utils/stylelint/warning-to-diagnostic.ts
index 22b3910..7d773e5 100644
--- a/src/utils/stylelint/warning-to-diagnostic.ts
+++ b/src/utils/stylelint/warning-to-diagnostic.ts
@@ -46,7 +46,10 @@ export function warningToDiagnostic(
 	ruleMetadata?: stylelint.LinterResult['ruleMetadata'],
 ): Diagnostic {
 	const start = Position.create(warning.line - 1, warning.column - 1);
-	const end = Position.create(warning.line - 1, warning.column);
+	const end =
+		typeof warning.endLine === 'number' && typeof warning.endColumn === 'number'
+			? Position.create(warning.endLine - 1, warning.endColumn - 1)
+			: Position.create(warning.line - 1, warning.column);
 
 	const ruleDocUrl = ruleMetadata?.[warning.rule]?.url;
 


Test patch:
diff --git a/src/utils/stylelint/__tests__/__snapshots__/stylelint-runner.ts.snap b/src/utils/stylelint/__tests__/__snapshots__/stylelint-runner.ts.snap
index eb93330..51b53eb 100644
--- a/src/utils/stylelint/__tests__/__snapshots__/stylelint-runner.ts.snap
+++ b/src/utils/stylelint/__tests__/__snapshots__/stylelint-runner.ts.snap
@@ -31,7 +31,7 @@ Object {
       "message": "Unexpected empty block (block-no-empty)",
       "range": Object {
         "end": Object {
-          "character": 3,
+          "character": 4,
           "line": 0,
         },
         "start": Object {
diff --git a/src/utils/stylelint/__tests__/__snapshots__/warning-to-diagnostic.ts.snap b/src/utils/stylelint/__tests__/__snapshots__/warning-to-diagnostic.ts.snap
index 0d9a8b2..3bd2e79 100644
--- a/src/utils/stylelint/__tests__/__snapshots__/warning-to-diagnostic.ts.snap
+++ b/src/utils/stylelint/__tests__/__snapshots__/warning-to-diagnostic.ts.snap
@@ -47,7 +47,7 @@ Object {
   "message": "Expected \\"#bbbbbb\\" to be \\"#bbb\\" (color-hex-length)",
   "range": Object {
     "end": Object {
-      "character": 19,
+      "character": 25,
       "line": 2,
     },
     "start": Object {
diff --git a/test/e2e/__tests__/__snapshots__/code-actions.ts.snap b/test/e2e/__tests__/__snapshots__/code-actions.ts.snap
index 9386e0f..9cb0cf8 100644
--- a/test/e2e/__tests__/__snapshots__/code-actions.ts.snap
+++ b/test/e2e/__tests__/__snapshots__/code-actions.ts.snap
@@ -19,6 +19,20 @@ exports[`Code actions should disable rules for a specific line with a comment on
 "
 `;
 
+exports[`Code actions should disable rules for a specific line with a comment on the same line 2`] = `
+"#!/usr/bin/env node
+/* stylelint-disable color-no-invalid-hex */
+/* eslint-disable node/shebang */
+'use strict';
+
+const css = css\`
+	.foo {
+		color: #00;
+	}
+\`;
+"
+`;
+
 exports[`Code actions should disable rules for an entire file 1`] = `
 "/* stylelint-disable indentation */
 a {
diff --git a/test/e2e/__tests__/__snapshots__/config-basedir.ts.snap b/test/e2e/__tests__/__snapshots__/config-basedir.ts.snap
index 0e760b2..df058eb 100644
--- a/test/e2e/__tests__/__snapshots__/config-basedir.ts.snap
+++ b/test/e2e/__tests__/__snapshots__/config-basedir.ts.snap
@@ -29,7 +29,7 @@ Array [
     "message": "Expected indentation of 8 spaces (indentation)",
     "range": Object {
       "end": Object {
-        "character": 3,
+        "character": 14,
         "line": 1,
       },
       "start": Object {
diff --git a/test/e2e/__tests__/__snapshots__/config-file.ts.snap b/test/e2e/__tests__/__snapshots__/config-file.ts.snap
index d39f18f..04c1a47 100644
--- a/test/e2e/__tests__/__snapshots__/config-file.ts.snap
+++ b/test/e2e/__tests__/__snapshots__/config-file.ts.snap
@@ -10,7 +10,7 @@ Array [
     "message": "Unexpected empty block (block-no-empty)",
     "range": Object {
       "end": Object {
-        "character": 3,
+        "character": 4,
         "line": 0,
       },
       "start": Object {
diff --git a/test/e2e/__tests__/__snapshots__/ignore-disables.ts.snap b/test/e2e/__tests__/__snapshots__/ignore-disables.ts.snap
index b59647d..fa82bc8 100644
--- a/test/e2e/__tests__/__snapshots__/ignore-disables.ts.snap
+++ b/test/e2e/__tests__/__snapshots__/ignore-disables.ts.snap
@@ -29,7 +29,7 @@ Array [
     "message": "Expected indentation of 4 spaces (indentation)",
     "range": Object {
       "end": Object {
-        "character": 3,
+        "character": 14,
         "line": 3,
       },
       "start": Object {
diff --git a/test/e2e/__tests__/__snapshots__/lint.ts.snap b/test/e2e/__tests__/__snapshots__/lint.ts.snap
index 2ae3651..5755060 100644
--- a/test/e2e/__tests__/__snapshots__/lint.ts.snap
+++ b/test/e2e/__tests__/__snapshots__/lint.ts.snap
@@ -26,7 +26,7 @@ Array [
     "message": "Unexpected invalid hex color \\"#y3\\" (color-no-invalid-hex)",
     "range": Object {
       "end": Object {
-        "character": 12,
+        "character": 14,
         "line": 6,
       },
       "start": Object {
@@ -45,7 +45,7 @@ Array [
     "message": "Expected indentation of 4 spaces (indentation)",
     "range": Object {
       "end": Object {
-        "character": 3,
+        "character": 13,
         "line": 2,
       },
       "start": Object {
@@ -69,7 +69,7 @@ Array [
     "message": "Expected indentation of 4 spaces (indentation)",
     "range": Object {
       "end": Object {
-        "character": 3,
+        "character": 13,
         "line": 2,
       },
       "start": Object {
diff --git a/test/e2e/__tests__/__snapshots__/report-descriptionless-disables.ts.snap b/test/e2e/__tests__/__snapshots__/report-descriptionless-disables.ts.snap
index 602fefa..7f1d464 100644
--- a/test/e2e/__tests__/__snapshots__/report-descriptionless-disables.ts.snap
+++ b/test/e2e/__tests__/__snapshots__/report-descriptionless-disables.ts.snap
@@ -7,7 +7,7 @@ Array [
     "message": "Disable for \\"indentation\\" is missing a description",
     "range": Object {
       "end": Object {
-        "character": 5,
+        "character": 48,
         "line": 2,
       },
       "start": Object {
diff --git a/test/e2e/__tests__/__snapshots__/report-invalid-scope-disables.ts.snap b/test/e2e/__tests__/__snapshots__/report-invalid-scope-disables.ts.snap
index e2b55ee..326ae8e 100644
--- a/test/e2e/__tests__/__snapshots__/report-invalid-scope-disables.ts.snap
+++ b/test/e2e/__tests__/__snapshots__/report-invalid-scope-disables.ts.snap
@@ -7,7 +7,7 @@ Array [
     "message": "Rule \\"foo\\" isn't enabled",
     "range": Object {
       "end": Object {
-        "character": 1,
+        "character": 36,
         "line": 0,
       },
       "start": Object {
@@ -23,7 +23,7 @@ Array [
     "message": "Rule \\"foo\\" isn't enabled",
     "range": Object {
       "end": Object {
-        "character": 1,
+        "character": 31,
         "line": 2,
       },
       "start": Object {
@@ -39,7 +39,7 @@ Array [
     "message": "Rule \\"foo\\" isn't enabled",
     "range": Object {
       "end": Object {
-        "character": 1,
+        "character": 26,
         "line": 4,
       },
       "start": Object {
diff --git a/test/e2e/__tests__/__snapshots__/report-needless-disables.ts.snap b/test/e2e/__tests__/__snapshots__/report-needless-disables.ts.snap
index 5bf7aa0..65f56e0 100644
--- a/test/e2e/__tests__/__snapshots__/report-needless-disables.ts.snap
+++ b/test/e2e/__tests__/__snapshots__/report-needless-disables.ts.snap
@@ -7,7 +7,7 @@ Array [
     "message": "Needless disable for \\"indentation\\"",
     "range": Object {
       "end": Object {
-        "character": 3,
+        "character": 55,
         "line": 2,
       },
       "start": Object {
@@ -23,7 +23,7 @@ Array [
     "message": "Needless disable for \\"indentation\\"",
     "range": Object {
       "end": Object {
-        "character": 1,
+        "character": 34,
         "line": 6,
       },
       "start": Object {
@@ -39,7 +39,7 @@ Array [
     "message": "Needless disable for \\"indentation\\"",
     "range": Object {
       "end": Object {
-        "character": 17,
+        "character": 55,
         "line": 14,
       },
       "start": Object {
@@ -55,7 +55,7 @@ Array [
     "message": "Needless disable for \\"indentation\\"",
     "range": Object {
       "end": Object {
-        "character": 1,
+        "character": 34,
         "line": 17,
       },
       "start": Object {
@@ -71,7 +71,7 @@ Array [
     "message": "Needless disable for \\"unknown\\"",
     "range": Object {
       "end": Object {
-        "character": 3,
+        "character": 55,
         "line": 2,
       },
       "start": Object {
diff --git a/test/e2e/__tests__/__snapshots__/validate.ts.snap b/test/e2e/__tests__/__snapshots__/validate.ts.snap
index 8827f0d..560d6bc 100644
--- a/test/e2e/__tests__/__snapshots__/validate.ts.snap
+++ b/test/e2e/__tests__/__snapshots__/validate.ts.snap
@@ -10,7 +10,7 @@ Array [
     "message": "Expected indentation of 4 spaces (indentation)",
     "range": Object {
       "end": Object {
-        "character": 3,
+        "character": 13,
         "line": 2,
       },
       "start": Object {
diff --git a/test/integration/stylelint-vscode/__snapshots__/test.ts.snap b/test/integration/stylelint-vscode/__snapshots__/test.ts.snap
index 0684945..d14ca11 100644
--- a/test/integration/stylelint-vscode/__snapshots__/test.ts.snap
+++ b/test/integration/stylelint-vscode/__snapshots__/test.ts.snap
@@ -1,29 +1,5 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`StylelintRunner should be resolved with diagnostic plugin rule URL 1`] = `
-Array [
-  Object {
-    "code": "scss/at-rule-no-unknown",
-    "codeDescription": Object {
-      "href": "https://github.com/stylelint-scss/stylelint-scss/blob/master/src/rules/at-rule-no-unknown",
-    },
-    "message": "Unexpected unknown at-rule \\"@unknown\\" (scss/at-rule-no-unknown)",
-    "range": Object {
-      "end": Object {
-        "character": 1,
-        "line": 0,
-      },
-      "start": Object {
-        "character": 0,
-        "line": 0,
-      },
-    },
-    "severity": 1,
-    "source": "Stylelint",
-  },
-]
-`;
-
 exports[`StylelintRunner should be resolved with diagnostics when it lints CSS successfully 1`] = `
 Array [
   Object {
@@ -53,7 +29,7 @@ Array [
     "message": "Expected indentation of 0 tabs (indentation)",
     "range": Object {
       "end": Object {
-        "character": 3,
+        "character": 14,
         "line": 0,
       },
       "start": Object {
@@ -74,7 +50,7 @@ Array [
     "message": "Unknown rule this-rule-does-not-exist.",
     "range": Object {
       "end": Object {
-        "character": 1,
+        "character": 4,
         "line": 0,
       },
       "start": Object {
@@ -90,7 +66,7 @@ Array [
     "message": "Unknown rule this-rule-also-does-not-exist.",
     "range": Object {
       "end": Object {
-        "character": 1,
+        "character": 4,
         "line": 0,
       },
       "start": Object {
@@ -182,7 +158,7 @@ Array [
     "message": "Expected \\"bold\\" to be \\"700\\" (font-weight-notation)",
     "range": Object {
       "end": Object {
-        "character": 30,
+        "character": 33,
         "line": 2,
       },
       "start": Object {
@@ -201,7 +177,7 @@ Array [
     "message": "Expected \\"normal\\" to be \\"400\\" (font-weight-notation)",
     "range": Object {
       "end": Object {
-        "character": 7,
+        "character": 12,
         "line": 4,
       },
       "start": Object {
@@ -225,7 +201,7 @@ Array [
     "message": "Unexpected unit (length-zero-no-unit)",
     "range": Object {
       "end": Object {
-        "character": 11,
+        "character": 12,
         "line": 3,
       },
       "start": Object {
@@ -250,7 +226,7 @@ Object {
       "message": "Unexpected unknown type selector \\"unknown\\" (selector-type-no-unknown)",
       "range": Object {
         "end": Object {
-          "character": 1,
+          "character": 7,
           "line": 1,
         },
         "start": Object {
@@ -306,7 +282,7 @@ Object {
       "message": "Expected indentation of 2 spaces (indentation)",
       "range": Object {
         "end": Object {
-          "character": 4,
+          "character": 12,
           "line": 1,
         },
         "start": Object {
@@ -329,7 +305,7 @@ Object {
       "message": "Disable for \\"indentation\\" is missing a description",
       "range": Object {
         "end": Object {
-          "character": 5,
+          "character": 48,
           "line": 2,
         },
         "start": Object {
@@ -345,7 +321,7 @@ Object {
       "message": "Disable for \\"indentation\\" is missing a description",
       "range": Object {
         "end": Object {
-          "character": 1,
+          "character": 34,
           "line": 5,
         },
         "start": Object {
@@ -361,7 +337,7 @@ Object {
       "message": "Disable for \\"indentation\\" is missing a description",
       "range": Object {
         "end": Object {
-          "character": 15,
+          "character": 53,
           "line": 11,
         },
         "start": Object {
@@ -384,7 +360,7 @@ Object {
       "message": "Rule \\"foo\\" isn't enabled",
       "range": Object {
         "end": Object {
-          "character": 1,
+          "character": 36,
           "line": 1,
         },
         "start": Object {
@@ -400,7 +376,7 @@ Object {
       "message": "Rule \\"foo\\" isn't enabled",
       "range": Object {
         "end": Object {
-          "character": 1,
+          "character": 31,
           "line": 3,
         },
         "start": Object {
@@ -416,7 +392,7 @@ Object {
       "message": "Rule \\"foo\\" isn't enabled",
       "range": Object {
         "end": Object {
-          "character": 1,
+          "character": 26,
           "line": 5,
         },
         "start": Object {
@@ -442,7 +418,7 @@ Object {
       "message": "Expected indentation of 4 spaces (indentation)",
       "range": Object {
         "end": Object {
-          "character": 3,
+          "character": 47,
           "line": 2,
         },
         "start": Object {
@@ -458,7 +434,7 @@ Object {
       "message": "Needless disable for \\"indentation\\"",
       "range": Object {
         "end": Object {
-          "character": 3,
+          "character": 46,
           "line": 2,
         },
         "start": Object {
@@ -474,7 +450,7 @@ Object {
       "message": "Needless disable for \\"indentation\\"",
       "range": Object {
         "end": Object {
-          "character": 1,
+          "character": 34,
           "line": 6,
         },
         "start": Object {
@@ -490,7 +466,7 @@ Object {
       "message": "Needless disable for \\"indentation\\"",
       "range": Object {
         "end": Object {
-          "character": 17,
+          "character": 55,
           "line": 13,
         },
         "start": Object {
@@ -506,7 +482,7 @@ Object {
       "message": "Needless disable for \\"indentation\\"",
       "range": Object {
         "end": Object {
-          "character": 1,
+          "character": 34,
           "line": 16,
         },
         "start": Object {
@@ -555,7 +531,7 @@ Object {
       "message": "Expected indentation of 2 spaces (indentation)",
       "range": Object {
         "end": Object {
-          "character": 4,
+          "character": 12,
           "line": 1,
         },
         "start": Object {
diff --git a/test/integration/stylelint-vscode/test.ts b/test/integration/stylelint-vscode/test.ts
index 36e2e65..a3bcb9d 100644
--- a/test/integration/stylelint-vscode/test.ts
+++ b/test/integration/stylelint-vscode/test.ts
@@ -3,6 +3,7 @@ import { pathToFileURL } from 'url';
 import { TextDocument } from 'vscode-languageserver-textdocument';
 import { StylelintRunner } from '../../../src/utils/stylelint';
 import { version as stylelintVersion } from 'stylelint/package.json';
+import { version as stylelintScssVersion } from 'stylelint-scss/package.json';
 import semver from 'semver';
 
 const createDocument = (uri: string | null, languageId: string, contents: string): TextDocument =>
@@ -264,23 +265,45 @@ a { color: #000 }
 		expect(result.diagnostics).toMatchSnapshot();
 	});
 
-	test('should be resolved with diagnostic plugin rule URL', async () => {
-		expect.assertions(1);
-		const runner = new StylelintRunner();
-		const result = await runner.lintDocument(
-			createDocument('unknown-rule.scss', 'scss', '@unknown (max-width: 960px) {}'),
-			{
-				config: {
-					plugins: ['stylelint-scss'],
-					rules: {
-						'scss/at-rule-no-unknown': true,
+	if (semver.satisfies(stylelintScssVersion, '^15')) {
+		test('should be resolved with diagnostic plugin rule URL', async () => {
+			expect.assertions(1);
+			const runner = new StylelintRunner();
+			const result = await runner.lintDocument(
+				createDocument('unknown-rule.scss', 'scss', '@unknown (max-width: 960px) {}'),
+				{
+					config: {
+						plugins: ['stylelint-scss'],
+						rules: {
+							'scss/at-rule-no-unknown': true,
+						},
 					},
 				},
-			},
-		);
+			);
 
-		expect(result.diagnostics).toMatchSnapshot();
-	});
+			expect(result.diagnostics).toEqual([
+				{
+					code: 'scss/at-rule-no-unknown',
+					codeDescription: {
+						href: 'https://github.com/stylelint-scss/stylelint-scss/blob/master/src/rules/at-rule-no-unknown',
+					},
+					message: 'Unexpected unknown at-rule "@unknown" (scss/at-rule-no-unknown)',
+					range: {
+						end: {
+							character: 8,
+							line: 0,
+						},
+						start: {
+							character: 0,
+							line: 0,
+						},
+					},
+					severity: 1,
+					source: 'Stylelint',
+				},
+			]);
+		});
+	}
 });
 
 describe('StylelintRunner with a configuration file', () => {