Skip to content

Commit 17cd8ba

Browse files
feat: report diagnostics for malformed PEP 723 inline script metadata (#1785)
Part of #1602 (PEP 723 inline script env support). Does not close it — this covers only diagnostics for malformed metadata. ## Problem Malformed PEP 723 metadata is currently **invisible**. The parser collapses every failure to `undefined`, so a block with a typo — a missing closing `# ///`, a content line without the required space after `#`, a TOML syntax error — produces no CodeLens, no warning, nothing. The file just looks like ordinary Python and the user has no idea why the "Setup environment" action never appeared. Worse, a malformed block and a file with *no* block were indistinguishable, so we couldn't have told the user even if we wanted to. ## What this does The parser now returns a discriminated result — `valid` / `invalid` / `none` — carrying structured problems with source ranges, and a new `InlineScriptDiagnosticsPublisher` surfaces them as squiggles. Gated behind the existing `python-envs.inlineScripts.enabled` setting (default `false`, latched at activation). No Pylance or Python extension changes; no cross-team dependencies. --- ## Spec conformance Normative source: [PyPA — Inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/). Permalinks below are pinned to commit [`9dd85f2`](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst), the latest commit touching that file, so line numbers can't drift. I extracted every RFC 2119 keyword sentence from the spec source programmatically: **20 total — 11 MUST, 2 MUST NOT, 1 SHOULD, 7 MAY.** The mapping below is against that complete set, not a sampling. ### Diagnostics backed by an explicit spec MUST | Code | Severity | Spec text | Strength | Source | |---|---|---|---|---| | `invalid-block-marker` | Error | "blocks that MUST start with the line `# /// TYPE`… Block MUST end with the line `# ///`… The `TYPE` MUST only consist of ASCII letters, numbers and hyphens" | MUST x3 | [L20-L26](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L20-L26) | | `invalid-content-line` | Error | "Every line between these two lines MUST be a comment starting with `#`. If there are characters after the `#` then the first character MUST be a space" | MUST x2 | [L28-L30](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L28-L30) | | `multiple-blocks` | Error | "When there are multiple comment blocks of the same `TYPE` defined, tools MUST produce an error" | MUST | [L54-L55](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L54-L55) | | `unterminated-block` | **Warning** | "Unclosed blocks MUST be ignored" | MUST | [L51-L52](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L51-L52) | `unterminated-block` is a **warning, not an error**, precisely because the spec says such blocks MUST be *ignored* — the file is valid Python and valid per spec. We only hint that the author probably meant to close it. See the scoping rule below. Also enforced without needing a diagnostic: | Spec requirement | Strength | How we conform | Source | |---|---|---|---| | "Tools MUST NOT read from metadata blocks with types that have not been standardized" | MUST NOT | `type === 'script'` filter in both the block regex and the opener scan | [L70-L71](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L70-L71) | | "If they choose not to [respect the encoding declaration], they MUST process the file as UTF-8" | MAY / MUST | We do not honour the encoding declaration; we process as UTF-8 and strip BOM | [L57-L58](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L57-L58) | | "Tools MAY choose to do a simple textual scan, rather than a full Python parse" | MAY | Textual scan, per the canonical regex | [L77](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L77) | | "the text specification takes precedence" over the canonical regex | tiebreak | Applied deliberately — see empty blocks below | [L67-L68](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L67-L68) | ### Deliberately NOT reported These are conformance decisions, not oversights. Each is measured by a test. | Case | Why we stay silent | |---|---| | A start line nested inside another block | Spec says tools **MAY** produce an error — MAY, not MUST. We take the least-aggressive legal option. [L51](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L51) | | **Empty block** (`# /// script` immediately followed by `# ///`) | The canonical regex requires >=1 content line (`+`), but the prose doesn't. Spec says prose wins, so this is **valid, empty metadata**. `BLOCK_RE`'s content group is `*` with a comment marking the deliberate deviation. | | Unclosed `# /// script` appearing *below* real code | Almost always a PEP 723 example inside a docstring or tutorial text, not a broken header. New `headerRegionEnd()` suppresses the warning at/after the first non-blank, non-`#` line. The spec explicitly says behaviour inside multi-line strings "is tool-dependent and should not be relied on". | | `# ///` used as block *content* | Spec's precedence rule plus its own embedded-C# example. Handled by regex backtracking; verified against the spec's literal example. | | Divider comments (`# /////////`), `## ///`, non-`script` types | Not blocks. | ### Checks that are **ours**, not the spec's Worth calling out explicitly so reviewers can push back: **`invalid-field-type`** (Error) fires when `dependencies` isn't an array of strings, `requires-python` isn't a string, or `tool` isn't a table. The spec describes these types in prose — "`dependencies`: A list of strings…", "`requires-python`: A string…" ([L99-L104](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L99-L104)) — **but attaches no MUST to the container type.** The only MUST in those bullets governs entry *validity*. I kept it as an error because a non-array `dependencies` has no valid interpretation and the alternative is a confusing downstream resolver failure. But it is inference, not mandate. Happy to downgrade to a warning if reviewers prefer strict spec-only conformance. **`invalid-toml`** (Error) — the spec never literally states the content is TOML; it's implied by the `[tool]` table semantics and the reference implementation's `tomllib.loads(content)`. ### Known gaps (not addressed here) Two genuine spec MUSTs are **not** enforced. Verified by measurement — both parse as valid with no diagnostic today: | Spec text | Source | Status | |---|---|---| | "Each entry MUST be a valid [dependency specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers/)" (PEP 508) | [L99-L101](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L99-L101) | Not validated — `dependencies = ["!!! not a specifier !!!"]` accepted silently | | "The value of this field MUST be a valid [version specifier](https://packaging.python.org/en/latest/specifications/version-specifiers/)" (PEP 440) | [L102-L104](https://github.com/pypa/packaging.python.org/blob/9dd85f27266c88c4eca58828c918c337b665d331/source/specifications/inline-script-metadata.rst#L102-L104) | Not validated — `requires-python = "totally-not-a-version"` accepted silently | Both need real grammar implementations and would meaningfully expand scope. Deferred intentionally; happy to file a follow-up issue. --- ## Behaviour change reviewers must notice This PR is **not purely additive.** One existing behaviour changes: ```python # /// script # /// ``` **Before:** parsed as no metadata -> no CodeLens. **After:** parses as valid, empty metadata -> **the setup CodeLens now appears.** That follows from the spec's "text takes precedence" tiebreak, and I believe it's correct, but it does affect provisioning and not just diagnostics. Everything else in this PR is diagnostics-only and leaves the provisioning path byte-identical — I verified that with a harness running the old and new parsers side by side over 20 scenarios. --- ## Implementation notes **`src/common/inlineScript/metadata.ts`** — the parser rework. - Result is now `{ kind: 'valid' | 'invalid' | 'none', problems, metadata? }`. - New independent opener scan (`findScriptOpeners`) — this is what makes "malformed block" distinguishable from "no block". The canonical regex alone can't tell them apart. - `toSourceRange` is the **single** offset-to-line/character translation point. Reviewers should focus here for range correctness. - `tomlErrorSourceRange` recovers `line`/`col` from `@iarna/toml` errors that were previously discarded, so the squiggle underlines the actual offending text instead of the whole block. **`src/features/inlineScript/diagnostics.ts`** — the publisher. - Publishes on open, save, and debounced change (**300 ms**, via the existing `createSimpleDebounce` helper). Debouncing matters here: without it we'd flash "missing closing marker" at someone who is still mid-edit. - `createSimpleDebounce(ms, cb).trigger()` takes no arguments and owns one timer, so a shared instance would let documents clobber each other — hence the per-URI `pending` map. - Clears on close, delete, rename, and dispose, so no stale squiggles survive. - Scope: `file:` scheme plus `.py` extension only. **Validation reads the live editor buffer**, so squiggles match what's on screen; provisioning keeps reading the saved file. Both call the same parser, so the two views can't disagree about what "valid" means. ### Edge cases handled - **BOM and CRLF** — BOM stripped, `\r\n?` normalized to `\n` before matching. Note JS regex `.` doesn't match `\r` while Python's does, so the canonical regex can't be used verbatim. - **Ranges under BOM/CRLF** — the bulk of the test suite. Every range assertion runs across four variants: LF, CRLF, BOM+LF, BOM+CRLF. - **8 KiB header slice** — diagnostics apply `sliceHeaderBytes` exactly as the provisioning path does, so the two can't diverge on large files. - **Adjacent blocks merge** — `# ///` is itself a legal content line, so back-to-back blocks are consumed as one. `multiple-blocks` correctly requires a non-content line between them. ### Known limitation Indented markers (` # /// script`) are silent. The spec anchors markers at column 0, so an indented one isn't a block at all. Documented in the parser. --- ## Testing - **52 new unit tests** (31 parser/range, 21 publisher). - Full suite: **2280 passing, 0 failing, 6 pending**, on a fresh `npm run compile-tests` build. - `npm run lint` clean, `tsc` clean, Prettier clean. > Note for anyone re-running: `npm run unittest` executes `./out/test/**/*.unit.test.js` — **compiled output**. Run `npm run compile-tests` first or you'll silently validate stale code. ### Manual verification Set `python-envs.inlineScripts.enabled: true` and **reload the window** (the flag is latched at activation). Then try: ```python # /// script # requires-python = ">=3.11" # dependencies = ["requests"] ``` -> warning: missing closing `# ///` ```python # /// script #requires-python = ">=3.11" # /// ``` -> error: content line must be exactly `#` or start with `# ` ```python # /// script # dependencies = [ # /// ``` -> error: TOML error, underlining the offending token ## Review focus 1. **`toSourceRange` correctness** — every range flows through it; BOM/CRLF offsets are the likeliest bug. 2. **Is `invalid-field-type` too aggressive?** It's the one error not backed by a spec MUST. 3. **The empty-block behaviour change** — it alters provisioning, not just diagnostics. 4. **Debounce lifecycle** — per-URI map, disposal on close/delete/rename. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 6ad1427 commit 17cd8ba

7 files changed

Lines changed: 1434 additions & 42 deletions

File tree

‎src/common/inlineScript/metadata.ts‎

Lines changed: 373 additions & 42 deletions
Large diffs are not rendered by default.

‎src/common/localize.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,50 @@ export namespace InlineScriptStrings {
3535
export const updatePythonAndPylanceExtensions = l10n.t(
3636
'The environment for this script was created. Update the Python and Pylance extensions for the full inline script experience.',
3737
);
38+
39+
export const diagnosticSource = l10n.t('Python Environments');
40+
41+
export const unterminatedBlock = l10n.t(
42+
"This '# /// script' block is missing its closing '# ///' marker, so its inline script metadata is ignored.",
43+
);
44+
export const multipleBlocks = l10n.t(
45+
"A script may contain only one '# /// script' block. Remove this block or merge it into the first one.",
46+
);
47+
export function invalidContentLine(line: string): string {
48+
const found = line.trim();
49+
if (found.length === 0) {
50+
return l10n.t(
51+
"Lines inside a '# /// script' block must be exactly '#' or start with '# '. This line is blank; use '#' for a blank metadata line.",
52+
);
53+
}
54+
return l10n.t(
55+
"Lines inside a '# /// script' block must be exactly '#' or start with '# '. Found: {0}",
56+
found,
57+
);
58+
}
59+
export function invalidBlockMarker(marker: string): string {
60+
return l10n.t(
61+
"'{0}' is not a valid inline script marker because of trailing whitespace. Markers must be exactly '# /// script' and '# ///'.",
62+
marker,
63+
);
64+
}
65+
export function invalidToml(detail: string): string {
66+
return l10n.t('The inline script metadata is not valid TOML: {0}', detail);
67+
}
68+
export function invalidFieldType(field: string): string {
69+
switch (field) {
70+
case 'requires-python':
71+
return l10n.t("Inline script metadata: 'requires-python' must be a string, for example '>=3.11'.");
72+
case 'dependencies':
73+
return l10n.t(
74+
"Inline script metadata: 'dependencies' must be an array of strings, for example ['requests'].",
75+
);
76+
case 'tool':
77+
return l10n.t("Inline script metadata: 'tool' must be a table.");
78+
default:
79+
return l10n.t("Inline script metadata: '{0}' has the wrong type.", field);
80+
}
81+
}
3882
}
3983

4084
export namespace Interpreter {

‎src/common/workspace.apis.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ export function onDidSaveTextDocument(
9595
return workspace.onDidSaveTextDocument(listener, thisArgs, disposables);
9696
}
9797

98+
export function onDidCloseTextDocument(
99+
listener: (e: TextDocument) => any,
100+
thisArgs?: any,
101+
disposables?: Disposable[],
102+
): Disposable {
103+
return workspace.onDidCloseTextDocument(listener, thisArgs, disposables);
104+
}
105+
98106
export function onDidChangeTextDocument(
99107
listener: (e: TextDocumentChangeEvent) => any,
100108
thisArgs?: any,

‎src/extension.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
import { PythonEnvironmentManagers } from './features/envManagers';
7070
import { EnvVarManager, PythonEnvVariableManager } from './features/execution/envVariableManager';
7171
import { latchInlineScriptFeatureActivation } from './features/inlineScript/activation';
72+
import { registerInlineScriptDiagnostics } from './features/inlineScript/diagnostics';
7273
import { InlineScriptLazyDetector } from './features/inlineScript/lazyDetector';
7374
import { registerInlineScriptUx } from './features/inlineScript/setupEnvironment';
7475
import {
@@ -229,6 +230,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
229230
inlineScriptLazyDetector.activate();
230231
context.subscriptions.push(inlineScriptLazyDetector);
231232

233+
if (inlineScriptFeatureActivation.enabled) {
234+
context.subscriptions.push(registerInlineScriptDiagnostics());
235+
}
236+
232237
setPythonApi(envManagers, projectManager, projectCreators, terminalManager, envVarManager);
233238
const api = await getPythonApi();
234239
const sysPythonManager = createDeferred<SysPythonManager>();
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
Diagnostic,
6+
DiagnosticCollection,
7+
DiagnosticSeverity,
8+
Disposable,
9+
languages,
10+
Range,
11+
TextDocument,
12+
Uri,
13+
} from 'vscode';
14+
import {
15+
InlineScriptMetadataProblem,
16+
parseInlineScriptMetadata,
17+
sliceHeaderBytes,
18+
} from '../../common/inlineScript/metadata';
19+
import { getInlineScriptRoutingKey } from '../../common/inlineScript/routingRegistry';
20+
import { InlineScriptStrings } from '../../common/localize';
21+
import { traceVerbose } from '../../common/logging';
22+
import { createSimpleDebounce, SimpleDebounce } from '../../common/utils/debounce';
23+
import { isSameOrParentPath } from '../../common/utils/pathUtils';
24+
import {
25+
getOpenTextDocuments,
26+
onDidChangeTextDocument,
27+
onDidCloseTextDocument,
28+
onDidDeleteFiles,
29+
onDidOpenTextDocument,
30+
onDidRenameFiles,
31+
onDidSaveTextDocument,
32+
} from '../../common/workspace.apis';
33+
34+
const VALIDATION_DEBOUNCE_MS = 300;
35+
36+
const DIAGNOSTIC_COLLECTION_NAME = 'python-envs-inline-script';
37+
38+
/** Publishes diagnostics for malformed PEP 723 inline script metadata, validating the live buffer. */
39+
export class InlineScriptDiagnosticsPublisher implements Disposable {
40+
private readonly subscriptions: Disposable[] = [];
41+
// `createSimpleDebounce` owns one timer, so a shared instance would let
42+
// edits in one file cancel another's pending validation.
43+
private readonly pending = new Map<string, { debounce: SimpleDebounce; document: TextDocument }>();
44+
private readonly published = new Map<string, Uri>();
45+
private disposed = false;
46+
47+
constructor(private readonly collection: DiagnosticCollection) {}
48+
49+
/** Documents open at activation are replayed via `setImmediate`, because `onLanguage:python` fires after editors are restored. */
50+
public activate(): void {
51+
this.subscriptions.push(
52+
onDidOpenTextDocument((doc) => this.validate(doc)),
53+
onDidSaveTextDocument((doc) => this.validate(doc)),
54+
onDidChangeTextDocument((e) => this.scheduleValidation(e.document)),
55+
onDidCloseTextDocument((doc) => this.clear(doc.uri)),
56+
onDidDeleteFiles((e) => e.files.forEach((uri) => this.clearTree(uri))),
57+
onDidRenameFiles((e) =>
58+
e.files.forEach((file) => {
59+
this.clearTree(file.oldUri);
60+
this.revalidateIfOpen(file.newUri);
61+
}),
62+
),
63+
);
64+
const handle = setImmediate(() => this.replayOpenDocuments());
65+
this.subscriptions.push(new Disposable(() => clearImmediate(handle)));
66+
}
67+
68+
public dispose(): void {
69+
this.disposed = true;
70+
this.subscriptions.forEach((s) => s.dispose());
71+
this.subscriptions.length = 0;
72+
this.pending.forEach((entry) => entry.debounce.dispose());
73+
this.pending.clear();
74+
this.published.clear();
75+
this.collection.dispose();
76+
}
77+
78+
private replayOpenDocuments(): void {
79+
if (this.disposed) {
80+
return;
81+
}
82+
const docs = getOpenTextDocuments().filter((doc) => shouldValidateUri(doc.uri));
83+
traceVerbose(`inlineScriptDiagnostics: activation replay over ${docs.length} candidate .py document(s)`);
84+
for (const doc of docs) {
85+
this.validate(doc);
86+
}
87+
}
88+
89+
private scheduleValidation(document: TextDocument): void {
90+
if (this.disposed || !shouldValidateUri(document.uri)) {
91+
return;
92+
}
93+
const key = document.uri.toString();
94+
const existing = this.pending.get(key);
95+
if (existing) {
96+
existing.document = document;
97+
existing.debounce.trigger();
98+
return;
99+
}
100+
const entry = {
101+
document,
102+
debounce: createSimpleDebounce(VALIDATION_DEBOUNCE_MS, () => {
103+
const queued = this.pending.get(key);
104+
this.pending.delete(key);
105+
if (queued) {
106+
this.validate(queued.document);
107+
}
108+
}),
109+
};
110+
this.pending.set(key, entry);
111+
entry.debounce.trigger();
112+
}
113+
114+
private validate(document: TextDocument): void {
115+
if (this.disposed || !shouldValidateUri(document.uri)) {
116+
return;
117+
}
118+
this.cancelPending(document.uri);
119+
120+
const uri = document.uri;
121+
const result = parseInlineScriptMetadata(sliceHeaderBytes(document.getText()), uri.fsPath);
122+
const problems = result.kind === 'none' ? [] : result.problems;
123+
if (problems.length === 0) {
124+
this.clear(uri);
125+
return;
126+
}
127+
128+
const diagnostics = problems.map((problem) => toDiagnostic(document, problem));
129+
this.collection.set(uri, diagnostics);
130+
this.published.set(uri.toString(), uri);
131+
traceVerbose(
132+
`inlineScriptDiagnostics: published ${diagnostics.length} problem(s) for ${uri.fsPath}: ` +
133+
problems.map((p) => p.code).join(', '),
134+
);
135+
}
136+
137+
private revalidateIfOpen(uri: Uri): void {
138+
if (this.disposed || !shouldValidateUri(uri)) {
139+
return;
140+
}
141+
const key = uri.toString();
142+
const doc = getOpenTextDocuments().find((d) => d.uri.toString() === key);
143+
if (doc) {
144+
this.validate(doc);
145+
}
146+
}
147+
148+
private clear(uri: Uri): void {
149+
this.cancelPending(uri);
150+
const key = uri.toString();
151+
if (this.published.delete(key)) {
152+
this.collection.delete(uri);
153+
}
154+
}
155+
156+
private clearTree(uri: Uri): void {
157+
this.clear(uri);
158+
if (uri.scheme !== 'file') {
159+
return;
160+
}
161+
// Pending entries have never published, so `published` alone would leave a
162+
// debounced validation to fire for a file that no longer exists.
163+
const tracked = [
164+
...this.published.values(),
165+
...Array.from(this.pending.values(), (entry) => entry.document.uri),
166+
];
167+
for (const candidate of tracked) {
168+
if (candidate.scheme === 'file' && isSameOrParentPath(uri.fsPath, candidate.fsPath)) {
169+
this.clear(candidate);
170+
}
171+
}
172+
}
173+
174+
private cancelPending(uri: Uri): void {
175+
const key = uri.toString();
176+
const entry = this.pending.get(key);
177+
if (entry) {
178+
entry.debounce.dispose();
179+
this.pending.delete(key);
180+
}
181+
}
182+
}
183+
184+
/** Whether inline script metadata is meaningful for `uri` — a local `.py` file. */
185+
export function shouldValidateUri(uri: Uri): boolean {
186+
return getInlineScriptRoutingKey(uri) !== undefined;
187+
}
188+
189+
function toDiagnostic(document: TextDocument, problem: InlineScriptMetadataProblem): Diagnostic {
190+
const range = new Range(
191+
document.positionAt(problem.sourceRange.start),
192+
document.positionAt(problem.sourceRange.end),
193+
);
194+
const diagnostic = new Diagnostic(range, messageFor(problem), severityFor(problem));
195+
diagnostic.source = InlineScriptStrings.diagnosticSource;
196+
diagnostic.code = problem.code;
197+
return diagnostic;
198+
}
199+
200+
function messageFor(problem: InlineScriptMetadataProblem): string {
201+
switch (problem.code) {
202+
case 'unterminated-block':
203+
return InlineScriptStrings.unterminatedBlock;
204+
case 'multiple-blocks':
205+
return InlineScriptStrings.multipleBlocks;
206+
case 'invalid-content-line':
207+
return InlineScriptStrings.invalidContentLine(problem.detail ?? '');
208+
case 'invalid-block-marker':
209+
return InlineScriptStrings.invalidBlockMarker(problem.detail ?? '');
210+
case 'invalid-toml':
211+
return InlineScriptStrings.invalidToml(problem.detail ?? '');
212+
case 'invalid-field-type':
213+
return InlineScriptStrings.invalidFieldType(problem.detail ?? '');
214+
default:
215+
return InlineScriptStrings.unterminatedBlock;
216+
}
217+
}
218+
219+
function severityFor(problem: InlineScriptMetadataProblem): DiagnosticSeverity {
220+
return problem.severity === 'warning' ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error;
221+
}
222+
223+
export function registerInlineScriptDiagnostics(): Disposable {
224+
const publisher = new InlineScriptDiagnosticsPublisher(
225+
languages.createDiagnosticCollection(DIAGNOSTIC_COLLECTION_NAME),
226+
);
227+
publisher.activate();
228+
return publisher;
229+
}

0 commit comments

Comments
 (0)