Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
node_modules/
test/*.js
23 changes: 23 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.json"],
"tsconfigRootDir": ".",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"env": {
"browser": true,
"es6": true,
"node": true
},
"ignorePatterns": ["dist/*", "node_modules/*"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ jobs:
with:
node-version: '20.x'
- run: npm install
- run: npm run lint
- run: npm run test
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
/.env
/Dockerfile
/compose.yaml
/.eslintignore
/.eslintrc.json
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

* Lint left- and right-side arguments of `contains`, `starts with`, `ends with` and `matches` operators
* Add eslint to make contributing/review easier

1.2
---
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"scripts": {
"pretest": "npm run-script prepare",
"test": "cm-runtests",
"prepare": "cm-buildhelper src/index.ts"
"prepare": "cm-buildhelper src/index.ts",
"lint-fix": "eslint . --fix --ext .ts,.js",
"lint": "eslint . --ext .ts,.js"
},
"type": "module",
"main": "dist/index.cjs",
Expand All @@ -28,7 +30,10 @@
"@codemirror/buildhelper": "^1.0.0",
"@types/mocha": "^10.0.10",
"@types/node": "^22.10.5",
"tsdoc-markdown": "^1.1.1"
"tsdoc-markdown": "^1.1.1",
"eslint": "^8.48.0",
"@typescript-eslint/parser": "^6.5.0",
"@typescript-eslint/eslint-plugin": "^6.5.0"
},
"license": "MIT",
"repository": {
Expand Down
6 changes: 3 additions & 3 deletions src/complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function completeIdentifier(state: EditorState, config: ExpressionLanguageConfig
};
}

function completeMember(state: EditorState, config: ExpressionLanguageConfig, tree: SyntaxNode, from: number, to: number, explicit: boolean): CompletionResult | null {
function completeMember(state: EditorState, config: ExpressionLanguageConfig, tree: SyntaxNode, from: number, to: number): CompletionResult | null {
if (!(tree.parent?.type.is(PropertyAccess) || tree.parent?.type.is(MethodAccess)) || !tree.parent?.firstChild) {
return null;
}
Expand All @@ -85,7 +85,7 @@ function completeMember(state: EditorState, config: ExpressionLanguageConfig, tr
return null;
}

let options = [];
const options = [];
for (const type of types) {
const typeDeclaration = config.types?.[type];
options.push(
Expand Down Expand Up @@ -117,7 +117,7 @@ export function expressionLanguageCompletion(context: CompletionContext): Comple
}

if ((prevNode.parent?.type.is(PropertyAccess) || prevNode.parent?.type.is(MethodAccess)) && [PropertyAccess, MethodAccess, ArrayAccess, Variable, Call, Application].includes(prevNode.parent.firstChild?.type.id)) {
return completeMember(state, config, prevNode, isIdentifier(prevNode) || isMember(prevNode) ? prevNode.from : pos, pos, explicit);
return completeMember(state, config, prevNode, isIdentifier(prevNode) || isMember(prevNode) ? prevNode.from : pos, pos);
}

if (
Expand Down
23 changes: 15 additions & 8 deletions src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import { Arguments, Method, Property, Variable, Function, BlockComment, BinaryEx
*/
export const expressionLanguageLinterSource = (state: EditorState) => {
const config = getExpressionLanguageConfig(state);
let diagnostics: Diagnostic[] = [];
const diagnostics: Diagnostic[] = [];

syntaxTree(state).cursor().iterate(node => {
const { from, to, type: { id } } = node;

let identifier: string | undefined;
switch (id) {
case 0:
case 0: {
if (state.doc.length === 0 || from === 0) {
// Don't show error on empty doc (even though it is an error)
return;
Expand All @@ -32,7 +32,8 @@ export const expressionLanguageLinterSource = (state: EditorState) => {
}

return;
case Arguments:
}
case Arguments: {
const fn = resolveFunctionDefinition(node.node.prevSibling, state, config);
const args = fn?.args;
if (!args) {
Expand All @@ -57,7 +58,12 @@ export const expressionLanguageLinterSource = (state: EditorState) => {
const typesExpected = args[i].type;

if (typesExpected && !typesExpected.includes(ELScalar.Any) && !typesUsed.some(x => typesExpected.includes(x))) {
diagnostics.push({ from: n.from, to: n.to, severity: 'error', message: `<code>${typesExpected.join('|')}</code> expected, got <code>${typesUsed.join('|')}</code>` });
diagnostics.push({
from: n.from,
to: n.to,
severity: 'error',
message: `<code>${typesExpected.join('|')}</code> expected, got <code>${typesUsed.join('|')}</code>`
});
}
i++;
}
Expand All @@ -67,8 +73,9 @@ export const expressionLanguageLinterSource = (state: EditorState) => {
}

break;
}
case Property:
case Method:
case Method: {
const leftArgument = node.node.parent?.firstChild?.node;
const types = Array.from(resolveTypes(state, leftArgument, config));
identifier = state.sliceDoc(from, to);
Expand All @@ -78,16 +85,16 @@ export const expressionLanguageLinterSource = (state: EditorState) => {
}

break;

}
case Variable:
case Function:
case Function: {
identifier = state.sliceDoc(from, node.node.firstChild ? node.node.firstChild.from - 1 : to);
if (!resolveIdentifier(id, identifier, config)) {
diagnostics.push({ from, to, severity: 'error', message: `${node.node.name} <code>${identifier}</code> not found` });
}

break;

}
case BinaryExpression: {
const operatorNode = node.node.getChild(OperatorKeyword);
if (operatorNode) {
Expand Down
2 changes: 1 addition & 1 deletion src/props.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NodeProp } from '@lezer/common';
import { ELScalar } from "./types";

// @ts-ignore
// @ts-expect-error TS2739
export const t: NodeProp<ELScalar> = {
deserialize: (str: string): ELScalar => str as ELScalar,
};
3 changes: 1 addition & 2 deletions src/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ function resolveArguments(node: SyntaxNode) {
function getCursorTooltips(state: EditorState): readonly Tooltip[] {
const config = getExpressionLanguageConfig(state);

// @ts-ignore
return state.selection.ranges
.filter(range => range.empty)
.map(range => {
Expand Down Expand Up @@ -55,7 +54,7 @@ function getCursorTooltips(state: EditorState): readonly Tooltip[] {
strictSide: false,
arrow: true,
create: () => {
let dom = document.createElement("div");
const dom = document.createElement("div");
dom.className = "cm-tooltip-cursor";
dom.textContent = `${argName}`;
return { dom };
Expand Down
10 changes: 5 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const resolveIdentifier = (nodeTypeId: typeof Method | typeof Property |
};

export function resolveTypes(state: EditorState, node: SyntaxNode | undefined | null, config: ExpressionLanguageConfig): Set<string> {
let types: Set<string> = new Set<string>();
const types: Set<string> = new Set<string>();
if (!node) {
return types;
}
Expand All @@ -85,22 +85,22 @@ export function resolveTypes(state: EditorState, node: SyntaxNode | undefined |
resolveTypes(state, node.firstChild, config).forEach(x => types.add(x));
} else if (node.type.is(Variable)) {
const varName = state.sliceDoc(node.from, node.to) || '';
// @ts-ignore
// @ts-expect-error TS2339
resolveIdentifier(node.type.id, varName, config)?.type?.forEach((x: string) => types.add(x));
} else if (node.type.is(Function)) {
const varName = state.sliceDoc(node.from, node.to) || '';
// @ts-ignore
// @ts-expect-error TS2339
resolveIdentifier(node.type.id, varName, config)?.returnType?.forEach((x: string) => types.add(x));
} else if (node.type.is(PropertyAccess) && node.firstChild && node.lastChild?.type.is(Property)) {
const varName = state.sliceDoc(node.lastChild.from, node.lastChild.to) || '';
resolveTypes(state, node.firstChild, config)?.forEach(baseType => {
// @ts-ignore
// @ts-expect-error TS2339
resolveIdentifier(node.lastChild?.type.id, varName, config.types?.[baseType])?.type?.forEach((x: string) => types.add(x));
});
} else if (node.type.is(MethodAccess) && node.firstChild && node.lastChild?.type.is(Method)) {
const varName = state.sliceDoc(node.lastChild.from, node.lastChild.to) || '';
resolveTypes(state, node.firstChild, config)?.forEach(baseType => {
// @ts-ignore
// @ts-expect-error TS2339
resolveIdentifier(node.lastChild?.type.id, varName, config.types?.[baseType])?.returnType?.forEach((x: string) => types.add(x));
});
}
Expand Down
24 changes: 12 additions & 12 deletions test/test-complete.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @ts-ignore
// @ts-expect-error TS2307
import { expressionlanguage } from "@valtzu/codemirror-lang-el";
import { EditorState } from "@codemirror/state";
import { Completion, CompletionContext, CompletionSource } from "@codemirror/autocomplete";
Expand All @@ -7,9 +7,9 @@ import ist from "ist";
const operatorKeywords = ['starts with', 'ends with', 'contains', 'matches', 'in', 'not', 'or', 'xor', 'and'];

async function get(doc: string, { explicit } = { explicit: false }) {
let cur = doc.indexOf("‸");
const cur = doc.indexOf("‸");
doc = doc.slice(0, cur) + doc.slice(cur + "‸".length);
let state = EditorState.create({
const state = EditorState.create({
doc,
selection: { anchor: cur },
extensions: [expressionlanguage({
Expand Down Expand Up @@ -41,31 +41,31 @@ async function get(doc: string, { explicit } = { explicit: false }) {

describe("Expression language completion", () => {
it("completes when explicitly requested", async () => {
let c = await get("‸", {explicit: true}) ?? [];
const c = await get("‸", {explicit: true}) ?? [];
ist(c.length, 0, '>');
ist(!c.some(o => operatorKeywords.includes(o.label)));
});

it("completes when explicitly requested, even when non-empty", async () => {
let c = await get("foo > 10 and ‸", {explicit: true}) ?? [];
const c = await get("foo > 10 and ‸", {explicit: true}) ?? [];
ist(c.length, 0, '>');
ist(!c.some(o => operatorKeywords.includes(o.label)));
});

it("completes operators when explicitly requested", async () => {
let c = await get("foo > 10 ‸", {explicit: true}) ?? [];
const c = await get("foo > 10 ‸", {explicit: true}) ?? [];
ist(c.length, 0, '>');
ist(c.some(o => operatorKeywords.includes(o.label)));
});

it("completes variables when explicitly requested, even mid-word", async () => {
let c = await get("foo > 10 and foo‸", {explicit: true}) ?? [];
const c = await get("foo > 10 and foo‸", {explicit: true}) ?? [];
ist(c.length, 0, '>');
ist(!c.some(o => operatorKeywords.includes(o.label)));
});

it("completes variables", async () => {
let c = await get("foo‸") ?? [];
const c = await get("foo‸") ?? [];
ist(c.length, 0, '>');
ist(c.map(x => x.label).includes('foobar'));
ist(c.map(x => x.label).includes('foobaz'));
Expand Down Expand Up @@ -144,7 +144,7 @@ describe("Expression language completion", () => {
});

it("completes object members after complex expression", async () => {
let c = await get("smash_my_head(obj.firstMethod()) + obj.‸") ?? [];
const c = await get("smash_my_head(obj.firstMethod()) + obj.‸") ?? [];
ist(c.length, 3);
ist("property11", c[0].label);
ist("property22", c[1].label);
Expand All @@ -164,23 +164,23 @@ describe("Expression language completion", () => {
});

it("does complete after ternary expression", async () => {
let c = await get("(foobar ? obj : false).‸") ?? [];
const c = await get("(foobar ? obj : false).‸") ?? [];
ist(c.length, 3);
ist("property11", c[0].label);
ist("property22", c[1].label);
ist("firstMethod()", c[2].label);
});

it("does complete after ternary expression shortcut", async () => {
let c = await get("(foobar ? obj).‸") ?? []
const c = await get("(foobar ? obj).‸") ?? []
ist(c.length, 3);
ist("property11", c[0].label);
ist("property22", c[1].label);
ist("firstMethod()", c[2].label);
});

it("does complete after ternary expression shortcut 2", async () => {
let c = await get("(foobar ?: obj).‸") ?? [];
const c = await get("(foobar ?: obj).‸") ?? [];
ist(c.length, 3);
ist("property11", c[0].label);
ist("property22", c[1].label);
Expand Down
11 changes: 5 additions & 6 deletions test/test-grammar.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
// @ts-ignore
// @ts-expect-error TS2307
import { ELLanguage } from "@valtzu/codemirror-lang-el";
import { fileTests } from "@lezer/generator/dist/test";

import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from 'url';
let caseDir = path.dirname(fileURLToPath(import.meta.url));
const caseDir = path.dirname(fileURLToPath(import.meta.url));

for (let file of fs.readdirSync(caseDir)) {
for (const file of fs.readdirSync(caseDir)) {
if (!/\.txt$/.test(file)) continue;

// @ts-ignore
let name = /^[^.]*/.exec(file)[0];
const name = /^[^.]*/.exec(file)[0];
describe(name, () => {
for (let { name, run } of fileTests(fs.readFileSync(path.join(caseDir, file), "utf8"), file)) {
for (const { name, run } of fileTests(fs.readFileSync(path.join(caseDir, file), "utf8"), file)) {
it(name, () => run(ELLanguage.parser));
}
});
Expand Down
2 changes: 1 addition & 1 deletion test/test-linter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @ts-ignore
// @ts-expect-error TS2307
import { expressionlanguage, ELLanguage, _linter } from "@valtzu/codemirror-lang-el";
import { EditorState } from "@codemirror/state";
import ist from "ist"
Expand Down
2 changes: 1 addition & 1 deletion test/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @ts-ignore
// @ts-expect-error TS2307
import { ELLanguage, expressionlanguage, _utils } from "@valtzu/codemirror-lang-el";
import { EditorState } from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
Expand Down