Skip to content

Commit

Permalink
fix: unjustified errors for property binding info (#639)
Browse files Browse the repository at this point in the history
* fix: unjustified error for property binding info

* chore: change set

* fix: review comments

* fix: add html equivalent
  • Loading branch information
marufrasully authored Jul 19, 2023
1 parent ccdedda commit 92e60aa
Show file tree
Hide file tree
Showing 14 changed files with 401 additions and 151 deletions.
8 changes: 8 additions & 0 deletions .changeset/shiny-maps-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@ui5-language-assistant/vscode-ui5-language-assistant-bas-ext": patch
"vscode-ui5-language-assistant": patch
"@ui5-language-assistant/binding-parser": patch
"@ui5-language-assistant/binding": patch
---

fix unjustified errors for property binding info
6 changes: 5 additions & 1 deletion packages/binding-parser/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export {
isBeforeAdjacentRange,
positionContained,
rangeContained,
} from "./utils/position";
isBindingExpression,
isMetadataPath,
isModel,
isPropertyBindingInfo,
} from "./utils";

import {
Value,
Expand Down
2 changes: 1 addition & 1 deletion packages/binding-parser/src/lexer/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const whiteSpace = createToken({
const specialChars = createToken({
name: SPECIAL_CHARS,
pattern:
/(?:#|!|"|\$|%|&|'|\(|\)|\*|\+|-|\.|\/|;|<|=|>|\?|@|\\|\^|_|`|~|\||)+/,
/(?:#|&gt;|&#47;|&#x2F;|!|"|\$|%|&|'|\(|\)|\*|\+|-|\.|\/|;|<|=|>|\?|@|\\|\^|_|`|~|\||)+/,
});

const leftCurly = createToken({
Expand Down
9 changes: 5 additions & 4 deletions packages/binding-parser/src/types/binding-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,15 @@ export interface Template {
spaces: WhiteSpaces[];
}

export interface ParseResultErrors {
lexer: LexerError[];
parse: ParseError[];
}
export interface ParseResult {
cst: CstNode;
ast: Template;
tokens: IToken[];
errors: {
lexer: LexerError[];
parse: ParseError[];
};
errors: ParseResultErrors;
}

export interface CreateToken<T = TokenType> {
Expand Down
148 changes: 148 additions & 0 deletions packages/binding-parser/src/utils/expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import type {
ParseResultErrors,
StructureValue,
} from "../types/binding-parser";
import { isAfterAdjacentRange, isBeforeAdjacentRange } from "./position";

/**
* Syntax of a binding expression can be represented by `{=expression}` or `{:=expression}`
* If an input text starts with either `{=` or `{:=`, input text is considered as binding expression
*/
export const isBindingExpression = (input: string): boolean => {
input = input.trim();
return /^{(=|:=)/.test(input);
};

/**
* Check model
*
* It is considered as model when it starts with `>` or its HTML equivalent after first key without any quotes e.g oData> or oData>/...
*/
export const isModel = (
binding: StructureValue,
errors?: ParseResultErrors
): boolean => {
if (!errors) {
return false;
}
const modelSign = errors.lexer.find(
(i) =>
i.type === "special-chars" &&
(i.text.startsWith(">") || i.text.startsWith("&gt;"))
);
if (!modelSign) {
return false;
}
// check model should appears after first key without quotes
if (
binding.elements[0]?.key?.originalText === binding.elements[0]?.key?.text &&
isBeforeAdjacentRange(binding.elements[0]?.key?.range, modelSign.range)
) {
return true;
}
return false;
};

/**
* Check metadata path
*
* It is considered metadata path when it is `/` or its HTML equivalent as separator and
*
* a. is before first key e.g /key
*
* b. is after first key e.g. key/
*/
export const isMetadataPath = (
binding: StructureValue,
errors?: ParseResultErrors
): boolean => {
if (!errors) {
return false;
}
const metadataSeparator = errors.lexer.find(
(i) =>
i.type === "special-chars" &&
(i.text.startsWith("/") ||
i.text.startsWith("&#47;") ||
i.text.startsWith("&#x2F;"))
);
if (!metadataSeparator) {
return false;
}
// check metadata separator is before first key e.g /key
if (
binding.elements[0]?.key?.range.start &&
isBeforeAdjacentRange(
metadataSeparator.range,
binding.elements[0].key.range
)
) {
return true;
}
// check metadata separator is after first key e.g. key/
if (
isAfterAdjacentRange(
metadataSeparator.range,
binding.elements[0]?.key?.range
)
) {
return true;
}
return false;
};

/**
* An input is considered property binding syntax when
*
* a. is empty curly bracket e.g `{}` or `{ }`
*
* b. has starting or closing curly bracket and key property with colon e.g `{anyKey: }` or `{"anyKey":}` or `{'anyKey':}`
*
* c. empty string [for initial code completion snippet]
*
* d. is not model e.g {i18n>...} or {oData>...}
*
* e. is not OData path e.g {/path/to/...} or {path/to/...}
*/
export const isPropertyBindingInfo = (
input: string,
binding?: StructureValue,
errors?: ParseResultErrors
): boolean => {
// check empty string
if (input.trim().length === 0) {
return true;
}

if (!binding) {
return false;
}

// check if model
if (isModel(binding, errors)) {
return false;
}

// check if metadata path
if (isMetadataPath(binding, errors)) {
return false;
}

if (
binding.leftCurly &&
binding.leftCurly.text &&
binding.elements.length === 0
) {
// check empty curly brackets
return true;
}
// check it has at least one key with colon
const result = binding.elements.find(
/* istanbul ignore next */
(item) => item.key?.text && item.colon?.text
);
if (result && binding.leftCurly && binding.leftCurly.text) {
return true;
}
return false;
};
14 changes: 14 additions & 0 deletions packages/binding-parser/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export {
isAfterAdjacentRange,
isBefore,
isBeforeAdjacentRange,
positionContained,
rangeContained,
} from "./position";

export {
isBindingExpression,
isMetadataPath,
isModel,
isPropertyBindingInfo,
} from "./expression";
172 changes: 172 additions & 0 deletions packages/binding-parser/test/unit/utils/expression.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { parseBinding } from "../../../src/parser";
import {
isBindingExpression,
isMetadataPath,
isModel,
isPropertyBindingInfo,
} from "../../../src/utils";

describe("expression", () => {
describe("isBindingExpression", () => {
it("check binding expression {=", () => {
const result = isBindingExpression("{=");
expect(result).toBeTrue();
});
it("check binding expression {:=", () => {
const result = isBindingExpression("{:=");
expect(result).toBeTrue();
});
it("check other false cases", () => {
const result = isBindingExpression("{");
expect(result).toBeFalse();
});
});
describe("isPropertyBindingInfo", () => {
it("empty string", () => {
const input = " ";
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeTrue();
});
it("string value", () => {
const input = "40";
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeFalse();
});
it("empty curly bracket without space", () => {
const input = "{}";
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeTrue();
});
it("empty curly bracket with space", () => {
const input = "{ }";
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeTrue();
});
it("key with colone [true]", () => {
const input = ' {path: "some/path"}';
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeTrue();
});
it("key with colone any where [true]", () => {
const input = ' {path "some/path", thisKey: {}}';
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeTrue();
});
it("missing colon [false]", () => {
const input = '{path "some/path"}';
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeFalse();
});
it("contains > after first key [false]", () => {
const input = "{i18n>myTestModel}";
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeFalse();
});
it("contains / before first key [false]", () => {
const input = "{/oData/path/to/some/dynamic/value}";
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeFalse();
});
it("contains / after first key [false]", () => {
const input = "{/oData/path/to/some/dynamic/value}";
const { ast, errors } = parseBinding(input);
const result = isPropertyBindingInfo(input, ast.bindings[0], errors);
expect(result).toBeFalse();
});
});

describe("isModel", () => {
it("return false if errors is undefined", () => {
const input = "{path: 'acceptable'}";
const { ast } = parseBinding(input);
expect(isModel(ast.bindings[0])).toBe(false);
});

it("return true if model sign appears after first key", () => {
const input = "{oData>/path/to/a/value}";
const { ast, errors } = parseBinding(input);
expect(isModel(ast.bindings[0], errors)).toBe(true);
});
it("return true if model sign as HTML equivalent appears after first key", () => {
const input = "{oData&gt;/path/to/a/value}";
const { ast, errors } = parseBinding(input);
expect(isModel(ast.bindings[0], errors)).toBe(true);
});

it("return false if model sign does not appear after first key", () => {
const input = "{i18n >}"; // space is not allowed
const { ast, errors } = parseBinding(input);
expect(isModel(ast.bindings[0], errors)).toBe(false);
});

it("return false if model sign is not found", () => {
const input = "{path: 'acceptable'}";
const { ast, errors } = parseBinding(input);
expect(isModel(ast.bindings[0], errors)).toBe(false);
});
it("return false if key is with single quote", () => {
const input = "{'path'>: ''}";
const { ast, errors } = parseBinding(input);
expect(isModel(ast.bindings[0], errors)).toBe(false);
});
it("return false if key is with double quotes", () => {
const input = '{"path">: ""}';
const { ast, errors } = parseBinding(input);
expect(isModel(ast.bindings[0], errors)).toBe(false);
});
it("return false if key is with single quote [HTML equivalent]", () => {
const input = "{&apos;path&apos;>: ''}";
const { ast, errors } = parseBinding(input);
expect(isModel(ast.bindings[0], errors)).toBe(false);
});
it("return false if key is with double quotes [HTML equivalent]", () => {
const input = "{&quot;path&quot;>: ''}";
const { ast, errors } = parseBinding(input);
expect(isModel(ast.bindings[0], errors)).toBe(false);
});
});
describe("isMetadataPath", () => {
it("return false if errors is undefined", () => {
const input = "{/path/to/a/value}'}";
const { ast } = parseBinding(input);
expect(isMetadataPath(ast.bindings[0])).toBe(false);
});
it("return false if there is no metadata separator", () => {
const input = "{path: 'acceptable'}";
const { ast, errors } = parseBinding(input);
expect(isMetadataPath(ast.bindings[0], errors)).toBe(false);
});

it("return true if the metadata separator is before adjacent first key", () => {
const input = "{/path/to/a/value}";
const { ast, errors } = parseBinding(input);
expect(isMetadataPath(ast.bindings[0], errors)).toBe(true);
});

it("return true if the metadata separator is after adjacent first key", () => {
const input = "{path/to/a/value}";
const { ast, errors } = parseBinding(input);
expect(isMetadataPath(ast.bindings[0], errors)).toBe(true);
});
it("return true if the metadata separator as HTML equivalent is before adjacent first key", () => {
const input = "{&#47;path/to/a/value}";
const { ast, errors } = parseBinding(input);
expect(isMetadataPath(ast.bindings[0], errors)).toBe(true);
});

it("return true if the metadata separator as HTML equivalent is after adjacent first key", () => {
const input = "{path&#x2F;to/a/value}";
const { ast, errors } = parseBinding(input);
expect(isMetadataPath(ast.bindings[0], errors)).toBe(true);
});
});
});
Loading

0 comments on commit 92e60aa

Please sign in to comment.