release: WRNexusJS 0.2.32
This commit is contained in:
@@ -9,6 +9,8 @@ const TOP_LEVEL_PATTERN = /^\s*(page|component|layout)\s+([A-Za-z_$][\w$]*)\s*\{
|
||||
|
||||
const VALID_TOP_LEVEL_KINDS = new Set(["page", "component", "layout"]);
|
||||
|
||||
const VALID_LIFECYCLE_HOOKS = new Set(["mount", "update", "unmount"]);
|
||||
|
||||
const VALID_MEMBERS = {
|
||||
page: new Set([
|
||||
"layout",
|
||||
@@ -23,9 +25,9 @@ const VALID_MEMBERS = {
|
||||
"realtime",
|
||||
]),
|
||||
|
||||
component: new Set(["props", "state", "view", "style", "functions"]),
|
||||
component: new Set(["props", "state", "view", "style", "functions", "lifecycle", "watch"]),
|
||||
|
||||
layout: new Set(["props", "state", "view", "style", "functions"]),
|
||||
layout: new Set(["props", "state", "view", "style", "functions", "lifecycle", "watch"]),
|
||||
};
|
||||
|
||||
function createDiagnostic(
|
||||
@@ -59,7 +61,6 @@ function lineDiagnostic(
|
||||
code,
|
||||
) {
|
||||
const line = document.lineAt(lineNumber);
|
||||
|
||||
const diagnostic = new vscode.Diagnostic(line.range, message, severity);
|
||||
|
||||
diagnostic.source = "WRNexus";
|
||||
@@ -316,6 +317,15 @@ function validateHtmlTags(document, source) {
|
||||
function getRootBodyRange(source, rootMatch) {
|
||||
const openingBrace = rootMatch.index + rootMatch[0].lastIndexOf("{");
|
||||
|
||||
const closingBrace = findMatchingBrace(source, openingBrace);
|
||||
|
||||
return {
|
||||
start: openingBrace + 1,
|
||||
end: closingBrace === -1 ? source.length : closingBrace,
|
||||
};
|
||||
}
|
||||
|
||||
function findMatchingBrace(source, openingBrace) {
|
||||
let depth = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
@@ -346,6 +356,17 @@ function getRootBodyRange(source, rootMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.startsWith("<!--", index)) {
|
||||
const commentEnd = source.indexOf("-->", index + 4);
|
||||
|
||||
if (commentEnd === -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
index = commentEnd + 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "{") {
|
||||
depth += 1;
|
||||
continue;
|
||||
@@ -355,22 +376,44 @@ function getRootBodyRange(source, rootMatch) {
|
||||
depth -= 1;
|
||||
|
||||
if (depth === 0) {
|
||||
return {
|
||||
start: openingBrace + 1,
|
||||
end: index,
|
||||
};
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function skipWhitespace(source, index, end) {
|
||||
while (index < end && /\s/.test(source[index])) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function readIdentifier(source, index, end) {
|
||||
if (index >= end || !/[A-Za-z_$]/.test(source[index])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const start = index;
|
||||
index += 1;
|
||||
|
||||
while (index < end && /[A-Za-z0-9_$-]/.test(source[index])) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
start: openingBrace + 1,
|
||||
end: source.length,
|
||||
name: source.slice(start, index),
|
||||
start,
|
||||
end: index,
|
||||
};
|
||||
}
|
||||
|
||||
function findRootMembers(source, bodyStart, bodyEnd) {
|
||||
const members = [];
|
||||
|
||||
let index = bodyStart;
|
||||
let depth = 0;
|
||||
let quote = null;
|
||||
@@ -414,7 +457,9 @@ function findRootMembers(source, bodyStart, bodyEnd) {
|
||||
|
||||
if (source.startsWith("<!--", index)) {
|
||||
const end = source.indexOf("-->", index + 4);
|
||||
|
||||
index = end === -1 ? bodyEnd : end + 3;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -430,25 +475,35 @@ function findRootMembers(source, bodyStart, bodyEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depth === 0 && /[A-Za-z_]/.test(character)) {
|
||||
const start = index;
|
||||
index += 1;
|
||||
if (depth === 0 && /[A-Za-z_$]/.test(character)) {
|
||||
const identifier = readIdentifier(source, index, bodyEnd);
|
||||
|
||||
while (index < bodyEnd && /[A-Za-z0-9_-]/.test(source[index])) {
|
||||
if (!identifier) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = source.slice(start, index);
|
||||
index = identifier.end;
|
||||
|
||||
members.push({
|
||||
name,
|
||||
start,
|
||||
end: index,
|
||||
name: identifier.name,
|
||||
start: identifier.start,
|
||||
end: identifier.end,
|
||||
});
|
||||
|
||||
// Single-line declarations must skip the rest of the line.
|
||||
if (name === "state" || name === "layout") {
|
||||
if (identifier.name === "state" || identifier.name === "layout") {
|
||||
skipLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (identifier.name === "watch") {
|
||||
index = skipWhitespace(source, index, bodyEnd);
|
||||
|
||||
const watchedState = readIdentifier(source, index, bodyEnd);
|
||||
|
||||
if (watchedState) {
|
||||
index = watchedState.end;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -460,6 +515,234 @@ function findRootMembers(source, bodyStart, bodyEnd) {
|
||||
return members;
|
||||
}
|
||||
|
||||
function findNamedBlocks(source, bodyStart, bodyEnd, blockName) {
|
||||
const blocks = [];
|
||||
let index = bodyStart;
|
||||
|
||||
while (index < bodyEnd) {
|
||||
index = skipWhitespace(source, index, bodyEnd);
|
||||
|
||||
const identifier = readIdentifier(source, index, bodyEnd);
|
||||
|
||||
if (!identifier) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
index = identifier.end;
|
||||
|
||||
if (identifier.name !== blockName) {
|
||||
const possibleBrace = skipWhitespace(source, index, bodyEnd);
|
||||
|
||||
if (source[possibleBrace] === "{") {
|
||||
const end = findMatchingBrace(source, possibleBrace);
|
||||
|
||||
index = end === -1 ? bodyEnd : end + 1;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const openingBrace = skipWhitespace(source, index, bodyEnd);
|
||||
|
||||
if (source[openingBrace] !== "{") {
|
||||
blocks.push({
|
||||
name: blockName,
|
||||
nameStart: identifier.start,
|
||||
nameEnd: identifier.end,
|
||||
openingBrace: -1,
|
||||
closingBrace: -1,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const closingBrace = findMatchingBrace(source, openingBrace);
|
||||
|
||||
blocks.push({
|
||||
name: blockName,
|
||||
nameStart: identifier.start,
|
||||
nameEnd: identifier.end,
|
||||
openingBrace,
|
||||
closingBrace,
|
||||
});
|
||||
|
||||
index = closingBrace === -1 ? bodyEnd : closingBrace + 1;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function findStateDeclarations(source, bodyStart, bodyEnd) {
|
||||
const states = new Map();
|
||||
let index = bodyStart;
|
||||
let depth = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
while (index < bodyEnd) {
|
||||
const character = source[index];
|
||||
|
||||
if (quote !== null) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (character === "\\") {
|
||||
escaped = true;
|
||||
} else if (character === quote) {
|
||||
quote = null;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '"' || character === "'") {
|
||||
quote = character;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.startsWith("<!--", index)) {
|
||||
const end = source.indexOf("-->", index + 4);
|
||||
|
||||
index = end === -1 ? bodyEnd : end + 3;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "{") {
|
||||
depth += 1;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "}") {
|
||||
depth = Math.max(0, depth - 1);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
depth === 0 &&
|
||||
source.startsWith("state", index) &&
|
||||
!/[A-Za-z0-9_$]/.test(source[index - 1] || "") &&
|
||||
!/[A-Za-z0-9_$]/.test(source[index + 5] || "")
|
||||
) {
|
||||
let cursor = skipWhitespace(source, index + 5, bodyEnd);
|
||||
|
||||
const state = readIdentifier(source, cursor, bodyEnd);
|
||||
|
||||
if (state) {
|
||||
states.set(state.name, state);
|
||||
index = state.end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
function findWatchDeclarations(source, bodyStart, bodyEnd) {
|
||||
const watches = [];
|
||||
let index = bodyStart;
|
||||
let depth = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
while (index < bodyEnd) {
|
||||
const character = source[index];
|
||||
|
||||
if (quote !== null) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (character === "\\") {
|
||||
escaped = true;
|
||||
} else if (character === quote) {
|
||||
quote = null;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '"' || character === "'") {
|
||||
quote = character;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.startsWith("<!--", index)) {
|
||||
const end = source.indexOf("-->", index + 4);
|
||||
|
||||
index = end === -1 ? bodyEnd : end + 3;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "{") {
|
||||
depth += 1;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "}") {
|
||||
depth = Math.max(0, depth - 1);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
depth === 0 &&
|
||||
source.startsWith("watch", index) &&
|
||||
!/[A-Za-z0-9_$]/.test(source[index - 1] || "") &&
|
||||
!/[A-Za-z0-9_$]/.test(source[index + 5] || "")
|
||||
) {
|
||||
const watchStart = index;
|
||||
let cursor = skipWhitespace(source, index + 5, bodyEnd);
|
||||
|
||||
const watchedState = readIdentifier(source, cursor, bodyEnd);
|
||||
|
||||
if (!watchedState) {
|
||||
watches.push({
|
||||
watchStart,
|
||||
watchEnd: index + 5,
|
||||
state: null,
|
||||
openingBrace: -1,
|
||||
closingBrace: -1,
|
||||
});
|
||||
|
||||
index += 5;
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor = skipWhitespace(source, watchedState.end, bodyEnd);
|
||||
|
||||
const openingBrace = source[cursor] === "{" ? cursor : -1;
|
||||
|
||||
const closingBrace = openingBrace === -1 ? -1 : findMatchingBrace(source, openingBrace);
|
||||
|
||||
watches.push({
|
||||
watchStart,
|
||||
watchEnd: index + 5,
|
||||
state: watchedState,
|
||||
openingBrace,
|
||||
closingBrace,
|
||||
});
|
||||
|
||||
index = closingBrace === -1 ? watchedState.end : closingBrace + 1;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return watches;
|
||||
}
|
||||
|
||||
function validateRootMembers(document, source, rootKind, rootMatch) {
|
||||
const diagnostics = [];
|
||||
const allowed = VALID_MEMBERS[rootKind];
|
||||
@@ -469,6 +752,7 @@ function validateRootMembers(document, source, rootKind, rootMatch) {
|
||||
}
|
||||
|
||||
const bodyRange = getRootBodyRange(source, rootMatch);
|
||||
|
||||
const members = findRootMembers(source, bodyRange.start, bodyRange.end);
|
||||
|
||||
for (const member of members) {
|
||||
@@ -491,8 +775,182 @@ function validateRootMembers(document, source, rootKind, rootMatch) {
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function validateLifecycleBlocks(document, source, rootKind, rootMatch) {
|
||||
if (rootKind !== "component" && rootKind !== "layout") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const diagnostics = [];
|
||||
const bodyRange = getRootBodyRange(source, rootMatch);
|
||||
|
||||
const blocks = findNamedBlocks(source, bodyRange.start, bodyRange.end, "lifecycle");
|
||||
|
||||
if (blocks.length > 1) {
|
||||
for (const block of blocks.slice(1)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
block.nameStart,
|
||||
block.nameEnd,
|
||||
"Only one `lifecycle { ... }` block is allowed.",
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-duplicate-lifecycle",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.openingBrace === -1) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
block.nameStart,
|
||||
block.nameEnd,
|
||||
"`lifecycle` must be followed by a block.",
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-invalid-lifecycle",
|
||||
),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const lifecycleEnd = block.closingBrace === -1 ? bodyRange.end : block.closingBrace;
|
||||
|
||||
let index = block.openingBrace + 1;
|
||||
const seenHooks = new Set();
|
||||
|
||||
while (index < lifecycleEnd) {
|
||||
index = skipWhitespace(source, index, lifecycleEnd);
|
||||
|
||||
if (index >= lifecycleEnd) {
|
||||
break;
|
||||
}
|
||||
|
||||
const hook = readIdentifier(source, index, lifecycleEnd);
|
||||
|
||||
if (!hook) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
index = hook.end;
|
||||
|
||||
if (!VALID_LIFECYCLE_HOOKS.has(hook.name)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
hook.start,
|
||||
hook.end,
|
||||
`Unknown lifecycle hook \`${hook.name}\`. Use \`mount\`, \`update\`, or \`unmount\`.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-invalid-lifecycle-hook",
|
||||
),
|
||||
);
|
||||
} else if (seenHooks.has(hook.name)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
hook.start,
|
||||
hook.end,
|
||||
`Duplicate lifecycle hook \`${hook.name}\`.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-duplicate-lifecycle-hook",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
seenHooks.add(hook.name);
|
||||
}
|
||||
|
||||
const openingBrace = skipWhitespace(source, index, lifecycleEnd);
|
||||
|
||||
if (source[openingBrace] !== "{") {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
hook.start,
|
||||
hook.end,
|
||||
`Lifecycle hook \`${hook.name}\` must be followed by a block.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-invalid-lifecycle-hook",
|
||||
),
|
||||
);
|
||||
|
||||
index = hook.end;
|
||||
continue;
|
||||
}
|
||||
|
||||
const closingBrace = findMatchingBrace(source, openingBrace);
|
||||
|
||||
index = closingBrace === -1 ? lifecycleEnd : closingBrace + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function validateWatchBlocks(document, source, rootKind, rootMatch) {
|
||||
if (rootKind !== "component" && rootKind !== "layout") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const diagnostics = [];
|
||||
const bodyRange = getRootBodyRange(source, rootMatch);
|
||||
|
||||
const states = findStateDeclarations(source, bodyRange.start, bodyRange.end);
|
||||
|
||||
const watches = findWatchDeclarations(source, bodyRange.start, bodyRange.end);
|
||||
|
||||
for (const watch of watches) {
|
||||
if (!watch.state) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
watch.watchStart,
|
||||
watch.watchEnd,
|
||||
"`watch` must be followed by a declared state name.",
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-invalid-watch",
|
||||
),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!states.has(watch.state.name)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
watch.state.start,
|
||||
watch.state.end,
|
||||
`Cannot watch undeclared state \`${watch.state.name}\`.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-unknown-watch-state",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (watch.openingBrace === -1) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
watch.state.start,
|
||||
watch.state.end,
|
||||
`Watcher for \`${watch.state.name}\` must be followed by a block.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-invalid-watch",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function validateRequiredView(document, source, rootKind, rootMatch) {
|
||||
const bodyRange = getRootBodyRange(source, rootMatch);
|
||||
|
||||
const body = source.slice(bodyRange.start, bodyRange.end);
|
||||
|
||||
if (/\bview\s*\{/.test(body)) {
|
||||
@@ -550,6 +1008,7 @@ function validateDocument(document) {
|
||||
|
||||
if (declaration.diagnostic) {
|
||||
diagnostics.push(declaration.diagnostic);
|
||||
|
||||
diagnostics.push(...validateBalancedCharacters(document, source));
|
||||
|
||||
return diagnostics;
|
||||
@@ -576,6 +1035,12 @@ function validateDocument(document) {
|
||||
|
||||
diagnostics.push(...validateRootMembers(document, source, declaration.kind, declaration.match));
|
||||
|
||||
diagnostics.push(
|
||||
...validateLifecycleBlocks(document, source, declaration.kind, declaration.match),
|
||||
);
|
||||
|
||||
diagnostics.push(...validateWatchBlocks(document, source, declaration.kind, declaration.match));
|
||||
|
||||
diagnostics.push(...validateRequiredView(document, source, declaration.kind, declaration.match));
|
||||
|
||||
diagnostics.push(...validateLayoutUsage(document, source, declaration.kind));
|
||||
@@ -593,19 +1058,20 @@ function registerDiagnostics(context) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousTimer = timers.get(document.uri.toString());
|
||||
const key = document.uri.toString();
|
||||
const previousTimer = timers.get(key);
|
||||
|
||||
if (previousTimer) {
|
||||
clearTimeout(previousTimer);
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
timers.delete(document.uri.toString());
|
||||
timers.delete(key);
|
||||
|
||||
collection.set(document.uri, validateDocument(document));
|
||||
}, 150);
|
||||
|
||||
timers.set(document.uri.toString(), timer);
|
||||
timers.set(key, timer);
|
||||
};
|
||||
|
||||
for (const document of vscode.workspace.textDocuments) {
|
||||
@@ -652,5 +1118,7 @@ module.exports = {
|
||||
validateBalancedCharacters,
|
||||
validateDocument,
|
||||
validateHtmlTags,
|
||||
validateLifecycleBlocks,
|
||||
validateRootMembers,
|
||||
validateWatchBlocks,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user