-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: unjustified errors for property binding info (#639)
* fix: unjustified error for property binding info * chore: change set * fix: review comments * fix: add html equivalent
- Loading branch information
1 parent
ccdedda
commit 92e60aa
Showing
14 changed files
with
401 additions
and
151 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(">")) | ||
); | ||
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("/") || | ||
i.text.startsWith("/")) | ||
); | ||
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
172
packages/binding-parser/test/unit/utils/expression.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>/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 = "{'path'>: ''}"; | ||
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 = "{"path">: ''}"; | ||
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 = "{/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/to/a/value}"; | ||
const { ast, errors } = parseBinding(input); | ||
expect(isMetadataPath(ast.bindings[0], errors)).toBe(true); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.