Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
981177f
update action config
Sep 8, 2025
67fb09a
update node and typescript
Sep 8, 2025
ff221af
add undefined check
Sep 8, 2025
c0fc7c2
fix mocha
Sep 8, 2025
ecef8cd
fix launch config
Sep 8, 2025
a3ce0ee
enable mocha tester in vscode
Sep 8, 2025
77fe063
0.11.0
Sep 8, 2025
d2c6085
simplify
Sep 8, 2025
cebbf1d
try reporting test results
Sep 8, 2025
6668ab8
simplify test summary
Sep 8, 2025
83f5100
checkpoint
Sep 8, 2025
c6eb414
checkpoint
Sep 8, 2025
4af63c8
fix promise implementation
Sep 9, 2025
5de58e3
update test settings
Sep 9, 2025
65983a9
checkpoint
Sep 9, 2025
ea6f6c4
Merge branch 'master' of https://github.com/microsoft/powerquery-lang…
Sep 9, 2025
d85e13f
regenerate lock file
Sep 9, 2025
ace79eb
0.11.1
Sep 9, 2025
1bc60b2
fix lint and async issues
Sep 9, 2025
243a6d9
remove unused
Sep 9, 2025
aac60a6
remove throwIfCancelled() from synchronous code paths
Sep 9, 2025
f9c1ff0
improve logic
Sep 9, 2025
1864238
Merge branch 'master' of https://github.com/microsoft/powerquery-lang…
Sep 9, 2025
f567c83
use expensive validation
Sep 9, 2025
9bfa8d1
instructions will be moved
Sep 10, 2025
18fe93b
refactor
Sep 10, 2025
c5890e5
Merge branch 'master' of https://github.com/microsoft/powerquery-lang…
Sep 11, 2025
274c3d0
cleanup
Sep 11, 2025
2f6de7e
don't reuse build window
Sep 11, 2025
40f8265
add yield helper
Sep 11, 2025
84d5335
remove conditional check
Sep 11, 2025
b8dbb6a
try this
Sep 11, 2025
b438cc5
use common code
Sep 11, 2025
89559b2
add timeout
Sep 11, 2025
fbedbb5
move cancellation token to test utils
Sep 11, 2025
14d2c24
redo tests
Sep 11, 2025
0412ece
large doc only
Sep 11, 2025
2e4a5b8
fix async cancellation
Sep 11, 2025
2a06ecd
change timeouts
Sep 11, 2025
f798a92
improve
Sep 11, 2025
278dc3b
adjust comment
Sep 11, 2025
0779958
cleanup comment
Sep 11, 2025
996ebab
test cleanup
Sep 11, 2025
291192a
add todo
Sep 11, 2025
8da8df5
remove the throwIfCancelled statements that were added to invokeExpre…
Sep 12, 2025
126aff9
Merge branch 'master' of https://github.com/microsoft/powerquery-lang…
Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
},

// VS Code Test Runner configuration
"testing.automaticallyOpenTestResults": "openOnTestStart",
"testing.automaticallyOpenPeekView": "failureInVisibleDocument",
"testing.automaticallyOpenTestResults": "openOnTestStart",
"testing.defaultGutterClickAction": "run",
"testing.followRunningTest": true,

Expand Down
3 changes: 3 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"clear": true
}
}
]
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module.exports = [
__dirname: "readonly",
module: "readonly",
setTimeout: "readonly",
console: "readonly",
},
},
plugins: {
Expand Down
15 changes: 0 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ async function inspectInvokeExpression(
correlationId,
);

settings.cancellationToken?.throwIfCancelled();

const invokeExpressionXorNode: TXorNode | undefined = NodeIdMapUtils.xor(nodeIdMapCollection, invokeExpressionId);

if (invokeExpressionXorNode === undefined) {
Expand All @@ -93,6 +91,8 @@ async function inspectInvokeExpression(
await tryType(settings, nodeIdMapCollection, previousNode.node.id, typeCache),
);

settings.cancellationToken?.throwIfCancelled();

let invokeExpressionArgs: InvokeExpressionArguments | undefined;

if (TypeUtils.isDefinedFunction(functionType)) {
Expand Down Expand Up @@ -213,6 +213,7 @@ async function getArgumentTypes(
for (const xorNode of argXorNodes) {
// eslint-disable-next-line no-await-in-loop
const triedArgType: TriedType = await tryType(settings, nodeIdMapCollection, xorNode.node.id, typeCache);
settings.cancellationToken?.throwIfCancelled();

if (ResultUtils.isError(triedArgType)) {
throw triedArgType;
Expand Down
45 changes: 45 additions & 0 deletions src/powerquery-language-services/promiseUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { ICancellationToken } from "@microsoft/powerquery-parser";
import { setImmediate } from "timers";

/**
* Sequential processing with cancellation support - often better than Promise.all
* for cancellation scenarios because we can stop between each operation.
* Also yields control before each operation to allow cancellation tokens to take effect.
*/
export async function processSequentiallyWithCancellation<T, R>(
items: T[],
processor: (item: T) => Promise<R>,
cancellationToken?: ICancellationToken,
): Promise<R[]> {
const results: R[] = [];

for (const item of items) {
// Yield control to allow async cancellation tokens to take effect
// eslint-disable-next-line no-await-in-loop
await yieldForCancellation(cancellationToken);

// eslint-disable-next-line no-await-in-loop
const result: R = await processor(item);
results.push(result);
}

return results;
}

export async function yieldForCancellation(cancellationToken?: ICancellationToken): Promise<void> {
if (cancellationToken) {
// First yield to microtasks (handles synchronous cancellation)
await Promise.resolve();
cancellationToken.throwIfCancelled();

// Additional yield for setImmediate-based cancellation tokens
// This ensures we yield to the timer queue where setImmediate callbacks execute
await new Promise<void>((resolve: () => void) => setImmediate(resolve));
cancellationToken.throwIfCancelled();
}

return Promise.resolve();
}
76 changes: 44 additions & 32 deletions src/powerquery-language-services/validate/validate.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { CommonError, Result, ResultUtils } from "@microsoft/powerquery-parser";
import { NodeIdMap, ParseError, ParseState } from "@microsoft/powerquery-parser/lib/powerquery-parser/parser";
import { Diagnostic } from "vscode-languageserver-types";
import { TextDocument } from "vscode-languageserver-textdocument";
import { Trace } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace";

import * as PromiseUtils from "../promiseUtils";

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we remove whitespace?

import { Analysis, AnalysisSettings, AnalysisUtils } from "../analysis";
import { CommonError, Result, ResultUtils } from "@microsoft/powerquery-parser";
import { TypeCache } from "../inspection";
import { validateDuplicateIdentifiers } from "./validateDuplicateIdentifiers";
import { validateFunctionExpression } from "./validateFunctionExpression";
Expand Down Expand Up @@ -35,7 +37,10 @@ export function validate(
initialCorrelationId: trace.id,
};

validationSettings.cancellationToken?.throwIfCancelled();

const analysis: Analysis = AnalysisUtils.analysis(textDocument, analysisSettings);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove whitespace?

const parseState: ParseState | undefined = ResultUtils.assertOk(await analysis.getParseState());
const parseError: ParseError.ParseError | undefined = ResultUtils.assertOk(await analysis.getParseError());

Expand All @@ -58,49 +63,56 @@ export function validate(
return result;
}

let functionExpressionDiagnostics: Diagnostic[];
let invokeExpressionDiagnostics: Diagnostic[];
let unknownIdentifiersDiagnostics: Diagnostic[];

const nodeIdMapCollection: NodeIdMap.Collection = parseState.contextState.nodeIdMapCollection;
const typeCache: TypeCache = analysis.getTypeCache();

if (validationSettings.checkInvokeExpressions && nodeIdMapCollection) {
functionExpressionDiagnostics = validateFunctionExpression(validationSettings, nodeIdMapCollection);
// Define validation operations to run sequentially
const validationOperations: (() => Promise<Diagnostic[]>)[] = [
// Parse validation (if there are parse errors)
async (): Promise<Diagnostic[]> => await validateParse(parseError, updatedSettings),
];

// Add conditional validations based on settings
if (validationSettings.checkForDuplicateIdentifiers && nodeIdMapCollection) {
validationOperations.push(
async (): Promise<Diagnostic[]> =>
await validateDuplicateIdentifiers(
textDocument,
nodeIdMapCollection,
updatedSettings,
validationSettings.cancellationToken,
),
);
}

invokeExpressionDiagnostics = await validateInvokeExpression(
validationSettings,
nodeIdMapCollection,
typeCache,
if (validationSettings.checkInvokeExpressions && nodeIdMapCollection) {
validationOperations.push(
async (): Promise<Diagnostic[]> =>
await validateFunctionExpression(validationSettings, nodeIdMapCollection),
async (): Promise<Diagnostic[]> =>
await validateInvokeExpression(validationSettings, nodeIdMapCollection, typeCache),
);
} else {
functionExpressionDiagnostics = [];
invokeExpressionDiagnostics = [];
}

if (validationSettings.checkUnknownIdentifiers && nodeIdMapCollection) {
unknownIdentifiersDiagnostics = await validateUnknownIdentifiers(
validationSettings,
nodeIdMapCollection,
typeCache,
validationOperations.push(
async (): Promise<Diagnostic[]> =>
await validateUnknownIdentifiers(validationSettings, nodeIdMapCollection, typeCache),
);
} else {
unknownIdentifiersDiagnostics = [];
}

// Execute all validation operations sequentially with cancellation support
const allDiagnostics: Diagnostic[][] = await PromiseUtils.processSequentiallyWithCancellation(
validationOperations,
(operation: () => Promise<Diagnostic[]>) => operation(),
validationSettings.cancellationToken,
);

// Flatten all diagnostics into a single array
const flattenedDiagnostics: Diagnostic[] = allDiagnostics.flat();

const result: ValidateOk = {
diagnostics: [
...validateDuplicateIdentifiers(
textDocument,
nodeIdMapCollection,
updatedSettings,
validationSettings.cancellationToken,
),
...(await validateParse(parseError, updatedSettings)),
...functionExpressionDiagnostics,
...invokeExpressionDiagnostics,
...unknownIdentifiersDiagnostics,
],
diagnostics: flattenedDiagnostics,
hasSyntaxError: parseError !== undefined,
};

Expand Down
Loading
Loading