@rettangoli/check 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +295 -0
- package/ROADMAP.md +175 -0
- package/package.json +46 -0
- package/src/cli/bin.js +325 -0
- package/src/cli/check.js +232 -0
- package/src/cli/index.js +1 -0
- package/src/core/analyze.js +227 -0
- package/src/core/discovery.js +83 -0
- package/src/core/exportedFunctions.js +235 -0
- package/src/core/model.js +898 -0
- package/src/core/parsers.js +2726 -0
- package/src/core/registry.js +779 -0
- package/src/core/schema.js +161 -0
- package/src/core/scopeGraph.js +1400 -0
- package/src/core/semantic.js +329 -0
- package/src/diagnostics/autofix.js +191 -0
- package/src/diagnostics/catalog.js +89 -0
- package/src/index.js +2 -0
- package/src/reporters/index.js +13 -0
- package/src/reporters/json.js +42 -0
- package/src/reporters/sarif.js +213 -0
- package/src/reporters/text.js +145 -0
- package/src/rules/compatibility.js +318 -0
- package/src/rules/constants.js +22 -0
- package/src/rules/crossFileSymbols.js +108 -0
- package/src/rules/expression.js +338 -0
- package/src/rules/feParity.js +65 -0
- package/src/rules/index.js +39 -0
- package/src/rules/jempl.js +80 -0
- package/src/rules/lifecycle.js +4 -0
- package/src/rules/listenerConfig.js +556 -0
- package/src/rules/listenerSymbols.js +49 -0
- package/src/rules/methods.js +117 -0
- package/src/rules/refs.js +20 -0
- package/src/rules/schema.js +118 -0
- package/src/rules/shared.js +20 -0
- package/src/rules/yahtmlAttrs.js +238 -0
- package/src/semantic/engine.js +778 -0
- package/src/semantic/index.js +9 -0
- package/src/types/lattice.js +281 -0
- package/src/utils/case.js +9 -0
- package/src/utils/fs.js +30 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { parseNamedExportedFunctions } from "../core/exportedFunctions.js";
|
|
2
|
+
import { getModelFilePath } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
const isObjectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
|
|
6
|
+
const getSchemaMethodMap = (model) => {
|
|
7
|
+
const normalized = model?.schema?.normalized?.methods?.byName;
|
|
8
|
+
if (normalized instanceof Map) {
|
|
9
|
+
return normalized;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const methodProperties = model?.schema?.yaml?.methods?.properties;
|
|
13
|
+
if (!isObjectRecord(methodProperties)) {
|
|
14
|
+
return new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return new Map(Object.entries(methodProperties));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const toRequiredObjectKeys = (schemaNode) => {
|
|
21
|
+
if (!isObjectRecord(schemaNode) || !Array.isArray(schemaNode.required)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return [...new Set(
|
|
26
|
+
schemaNode.required
|
|
27
|
+
.filter((entry) => typeof entry === "string" && entry.trim() === entry && entry.length > 0),
|
|
28
|
+
)].sort((left, right) => left.localeCompare(right));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const runMethodRules = ({ models = [] }) => {
|
|
32
|
+
const diagnostics = [];
|
|
33
|
+
|
|
34
|
+
models.forEach((model) => {
|
|
35
|
+
const methodsPath = getModelFilePath({ model, fileType: "methods" });
|
|
36
|
+
const exportedFunctions = parseNamedExportedFunctions({
|
|
37
|
+
sourceText: model?.methods?.sourceText || "",
|
|
38
|
+
filePath: methodsPath,
|
|
39
|
+
});
|
|
40
|
+
const methodSchemaByName = getSchemaMethodMap(model);
|
|
41
|
+
|
|
42
|
+
methodSchemaByName.forEach((methodSchema, methodName) => {
|
|
43
|
+
if (!model?.methods?.exports?.has(methodName)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const functionMeta = exportedFunctions.get(methodName);
|
|
48
|
+
if (!functionMeta) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
functionMeta.firstParam
|
|
54
|
+
&& !["identifier", "object", "unknown", "none"].includes(functionMeta.firstParam.kind)
|
|
55
|
+
) {
|
|
56
|
+
diagnostics.push({
|
|
57
|
+
code: "RTGL-CHECK-METHOD-001",
|
|
58
|
+
severity: "error",
|
|
59
|
+
filePath: methodsPath,
|
|
60
|
+
line: functionMeta.line,
|
|
61
|
+
message: `${model.componentKey}: method '${methodName}' must use an object payload parameter (identifier or object destructuring).`,
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (functionMeta.paramCount > 1) {
|
|
67
|
+
diagnostics.push({
|
|
68
|
+
code: "RTGL-CHECK-METHOD-002",
|
|
69
|
+
severity: "warn",
|
|
70
|
+
filePath: methodsPath,
|
|
71
|
+
line: functionMeta.line,
|
|
72
|
+
message: `${model.componentKey}: method '${methodName}' defines ${functionMeta.paramCount} parameters; runtime method invocation passes a single payload object.`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const requiredPayloadKeys = toRequiredObjectKeys(methodSchema?.payload);
|
|
77
|
+
if (requiredPayloadKeys.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (functionMeta.paramCount < 1) {
|
|
82
|
+
diagnostics.push({
|
|
83
|
+
code: "RTGL-CHECK-METHOD-003",
|
|
84
|
+
severity: "error",
|
|
85
|
+
filePath: methodsPath,
|
|
86
|
+
line: functionMeta.line,
|
|
87
|
+
message: `${model.componentKey}: method '${methodName}' payload contract requires keys [${requiredPayloadKeys.join(", ")}] but method has no payload parameter.`,
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (functionMeta.firstParam?.kind !== "object") {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (functionMeta.firstParam.hasRest) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const presentKeys = new Set(functionMeta.firstParam.objectKeys || []);
|
|
101
|
+
const missingKeys = requiredPayloadKeys.filter((key) => !presentKeys.has(key));
|
|
102
|
+
if (missingKeys.length === 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
diagnostics.push({
|
|
107
|
+
code: "RTGL-CHECK-METHOD-003",
|
|
108
|
+
severity: "error",
|
|
109
|
+
filePath: methodsPath,
|
|
110
|
+
line: functionMeta.line,
|
|
111
|
+
message: `${model.componentKey}: method '${methodName}' payload contract missing required key(s) [${missingKeys.join(", ")}].`,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return diagnostics;
|
|
117
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRefMatchers } from "@rettangoli/fe/contracts";
|
|
2
|
+
import { isObjectRecord } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
export const collectInvalidRefKeys = (refs) => {
|
|
5
|
+
const invalidRefKeys = new Set();
|
|
6
|
+
|
|
7
|
+
if (!isObjectRecord(refs)) {
|
|
8
|
+
return invalidRefKeys;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
Object.entries(refs).forEach(([refKey, refConfig]) => {
|
|
12
|
+
try {
|
|
13
|
+
createRefMatchers({ [refKey]: refConfig });
|
|
14
|
+
} catch {
|
|
15
|
+
invalidRefKeys.add(refKey);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return invalidRefKeys;
|
|
20
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { getModelFilePath, getYamlPathLine, isObjectRecord } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
const CUSTOM_ELEMENT_NAME_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
|
|
4
|
+
|
|
5
|
+
export const runSchemaRules = ({ models = [] }) => {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
const componentNameOwners = new Map();
|
|
8
|
+
|
|
9
|
+
models.forEach((model) => {
|
|
10
|
+
const schema = model?.schema?.yaml;
|
|
11
|
+
const schemaPath = getModelFilePath({ model, fileType: "schema" });
|
|
12
|
+
const schemaKeyPathLines = model?.schema?.yamlKeyPathLines || new Map();
|
|
13
|
+
const componentNameLine = getYamlPathLine(schemaKeyPathLines, ["componentName"]);
|
|
14
|
+
|
|
15
|
+
if (schema === null || schema === undefined) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!isObjectRecord(schema)) {
|
|
20
|
+
diagnostics.push({
|
|
21
|
+
code: "RTGL-CHECK-SCHEMA-001",
|
|
22
|
+
severity: "error",
|
|
23
|
+
filePath: schemaPath,
|
|
24
|
+
message: `${model.componentKey}: schema must be a YAML object.`,
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const componentName = schema.componentName;
|
|
30
|
+
const normalizedComponentName = typeof componentName === "string" ? componentName.trim() : "";
|
|
31
|
+
|
|
32
|
+
if (typeof componentName !== "string" || normalizedComponentName === "") {
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
code: "RTGL-CHECK-SCHEMA-002",
|
|
35
|
+
severity: "error",
|
|
36
|
+
filePath: schemaPath,
|
|
37
|
+
line: componentNameLine,
|
|
38
|
+
message: `${model.componentKey}: componentName is required.`,
|
|
39
|
+
});
|
|
40
|
+
} else if (componentName !== normalizedComponentName || !CUSTOM_ELEMENT_NAME_REGEX.test(componentName)) {
|
|
41
|
+
diagnostics.push({
|
|
42
|
+
code: "RTGL-CHECK-SCHEMA-003",
|
|
43
|
+
severity: "error",
|
|
44
|
+
filePath: schemaPath,
|
|
45
|
+
line: componentNameLine,
|
|
46
|
+
message: `${model.componentKey}: componentName '${componentName}' must be a valid custom-element tag (kebab-case with at least one '-').`,
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
if (!componentNameOwners.has(componentName)) {
|
|
50
|
+
componentNameOwners.set(componentName, []);
|
|
51
|
+
}
|
|
52
|
+
componentNameOwners.get(componentName).push({
|
|
53
|
+
filePath: schemaPath,
|
|
54
|
+
line: componentNameLine,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Object.prototype.hasOwnProperty.call(schema, "attrsSchema")) {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
code: "RTGL-CHECK-SCHEMA-004",
|
|
61
|
+
severity: "error",
|
|
62
|
+
filePath: schemaPath,
|
|
63
|
+
line: getYamlPathLine(schemaKeyPathLines, ["attrsSchema"]),
|
|
64
|
+
message: `${model.componentKey}: attrsSchema is not supported.`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (Object.prototype.hasOwnProperty.call(schema, "methods")) {
|
|
69
|
+
const methodsSchema = schema.methods;
|
|
70
|
+
if (!isObjectRecord(methodsSchema)) {
|
|
71
|
+
diagnostics.push({
|
|
72
|
+
code: "RTGL-CHECK-SCHEMA-005",
|
|
73
|
+
severity: "error",
|
|
74
|
+
filePath: schemaPath,
|
|
75
|
+
line: getYamlPathLine(schemaKeyPathLines, ["methods"]),
|
|
76
|
+
message: `${model.componentKey}: methods must be an object schema with a properties map.`,
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
if (Object.prototype.hasOwnProperty.call(methodsSchema, "type") && methodsSchema.type !== "object") {
|
|
80
|
+
diagnostics.push({
|
|
81
|
+
code: "RTGL-CHECK-SCHEMA-006",
|
|
82
|
+
severity: "error",
|
|
83
|
+
filePath: schemaPath,
|
|
84
|
+
line: getYamlPathLine(schemaKeyPathLines, ["methods", "type"]),
|
|
85
|
+
message: `${model.componentKey}: methods.type must be 'object'.`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!isObjectRecord(methodsSchema.properties)) {
|
|
90
|
+
diagnostics.push({
|
|
91
|
+
code: "RTGL-CHECK-SCHEMA-007",
|
|
92
|
+
severity: "error",
|
|
93
|
+
filePath: schemaPath,
|
|
94
|
+
line: getYamlPathLine(schemaKeyPathLines, ["methods", "properties"]),
|
|
95
|
+
message: `${model.componentKey}: methods.properties must be an object keyed by method name.`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
componentNameOwners.forEach((owners, componentName) => {
|
|
103
|
+
if (owners.length < 2) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
owners.forEach(({ filePath, line }) => {
|
|
107
|
+
diagnostics.push({
|
|
108
|
+
code: "RTGL-CHECK-SCHEMA-008",
|
|
109
|
+
severity: "error",
|
|
110
|
+
filePath,
|
|
111
|
+
line,
|
|
112
|
+
message: `Duplicate componentName '${componentName}' found in multiple schema files.`,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return diagnostics;
|
|
118
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const YAML_PATH_SEPARATOR = "\u0000";
|
|
2
|
+
|
|
3
|
+
const toYamlPathKey = (parts = []) => {
|
|
4
|
+
return parts.join(YAML_PATH_SEPARATOR);
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const isObjectRecord = (value) => {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getYamlPathLine = (keyPathLines, parts = []) => {
|
|
12
|
+
if (!(keyPathLines instanceof Map)) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return keyPathLines.get(toYamlPathKey(parts));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getModelFilePath = ({ model, fileType = "view" } = {}) => {
|
|
19
|
+
return model?.[fileType]?.filePath || model?.entries?.[0]?.filePath || model?.componentKey || "unknown";
|
|
20
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { toCamelCase, toKebabCase } from "../utils/case.js";
|
|
2
|
+
|
|
3
|
+
const GENERIC_ALLOWED_ATTRS = new Set([
|
|
4
|
+
"accesskey",
|
|
5
|
+
"autocapitalize",
|
|
6
|
+
"autofocus",
|
|
7
|
+
"id",
|
|
8
|
+
"class",
|
|
9
|
+
"contenteditable",
|
|
10
|
+
"dir",
|
|
11
|
+
"draggable",
|
|
12
|
+
"enterkeyhint",
|
|
13
|
+
"exportparts",
|
|
14
|
+
"hidden",
|
|
15
|
+
"inert",
|
|
16
|
+
"inputmode",
|
|
17
|
+
"lang",
|
|
18
|
+
"nonce",
|
|
19
|
+
"part",
|
|
20
|
+
"popover",
|
|
21
|
+
"itemid",
|
|
22
|
+
"itemprop",
|
|
23
|
+
"itemref",
|
|
24
|
+
"itemscope",
|
|
25
|
+
"itemtype",
|
|
26
|
+
"spellcheck",
|
|
27
|
+
"style",
|
|
28
|
+
"slot",
|
|
29
|
+
"tabindex",
|
|
30
|
+
"title",
|
|
31
|
+
"translate",
|
|
32
|
+
"name",
|
|
33
|
+
"key",
|
|
34
|
+
"href",
|
|
35
|
+
"new-tab",
|
|
36
|
+
"rel",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const normalizeBindingName = (bindingName = "") => {
|
|
40
|
+
if (bindingName.startsWith("@")) {
|
|
41
|
+
return {
|
|
42
|
+
sourceType: "event",
|
|
43
|
+
name: bindingName.slice(1),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (bindingName.startsWith(":")) {
|
|
47
|
+
return {
|
|
48
|
+
sourceType: "prop",
|
|
49
|
+
name: bindingName.slice(1),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (bindingName.startsWith("?")) {
|
|
53
|
+
return {
|
|
54
|
+
sourceType: "boolean-attr",
|
|
55
|
+
name: bindingName.slice(1),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (bindingName.startsWith(".")) {
|
|
59
|
+
return {
|
|
60
|
+
sourceType: "legacy-prop",
|
|
61
|
+
name: bindingName.slice(1),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
sourceType: "attr",
|
|
66
|
+
name: bindingName,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const isDynamicToken = (value = "") => {
|
|
71
|
+
return (
|
|
72
|
+
value.includes("${")
|
|
73
|
+
|| value.includes("#{")
|
|
74
|
+
|| value.includes("{{")
|
|
75
|
+
|| value.includes("}")
|
|
76
|
+
|| value.includes("{")
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const toCompactLower = (value = "") => {
|
|
81
|
+
return String(value).replaceAll("-", "").toLowerCase();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const getGenericAttrCandidates = (attrName = "") => {
|
|
85
|
+
if (!attrName) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const raw = String(attrName);
|
|
90
|
+
const lower = raw.toLowerCase();
|
|
91
|
+
const kebab = toKebabCase(raw);
|
|
92
|
+
const compact = toCompactLower(kebab);
|
|
93
|
+
|
|
94
|
+
return [...new Set([raw, lower, kebab, compact])];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const isGenericAllowed = (attrName = "") => {
|
|
98
|
+
if (!attrName) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return getGenericAttrCandidates(attrName).some((candidate) => {
|
|
103
|
+
if (GENERIC_ALLOWED_ATTRS.has(candidate)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (candidate.startsWith("aria-") || candidate.startsWith("data-")) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return candidate === "role";
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const isAllowedByContract = ({ attrName, contract }) => {
|
|
116
|
+
const raw = attrName;
|
|
117
|
+
const kebab = toKebabCase(attrName);
|
|
118
|
+
const camel = toCamelCase(attrName);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
contract.attrs.has(raw)
|
|
122
|
+
|| contract.attrs.has(kebab)
|
|
123
|
+
|| contract.attrs.has(camel)
|
|
124
|
+
|| contract.props.has(raw)
|
|
125
|
+
|| contract.props.has(kebab)
|
|
126
|
+
|| contract.props.has(camel)
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const runYahtmlAttrRules = ({ models = [], registry = new Map() }) => {
|
|
131
|
+
const diagnostics = [];
|
|
132
|
+
|
|
133
|
+
models.forEach((model) => {
|
|
134
|
+
const templateNodes = Array.isArray(model?.view?.templateAst?.nodes)
|
|
135
|
+
? model.view.templateAst.nodes
|
|
136
|
+
: [];
|
|
137
|
+
const nodeByKey = new Map();
|
|
138
|
+
templateNodes.forEach((node) => {
|
|
139
|
+
const key = `${node?.range?.line || 0}::${node?.rawKey || ""}`;
|
|
140
|
+
nodeByKey.set(key, node);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
model.view.selectorBindings.forEach((bindingLine) => {
|
|
144
|
+
const { tagName, bindingNames, filePath, line } = bindingLine;
|
|
145
|
+
if (!tagName || !tagName.includes("-")) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const contract = registry.get(tagName);
|
|
150
|
+
if (!contract) {
|
|
151
|
+
diagnostics.push({
|
|
152
|
+
code: "RTGL-CHECK-YAHTML-001",
|
|
153
|
+
severity: "error",
|
|
154
|
+
filePath,
|
|
155
|
+
line,
|
|
156
|
+
message: `${model.componentKey}: unknown custom element '${tagName}' in view template.`,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const astNode = nodeByKey.get(`${line || 0}::${bindingLine.rawKey || ""}`);
|
|
162
|
+
const astAttributes = Array.isArray(astNode?.attributes) ? astNode.attributes : [];
|
|
163
|
+
const astBindingNames = astAttributes
|
|
164
|
+
.map((attribute) => attribute?.bindingName)
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
const allBindingNames = [...new Set([...(bindingNames || []), ...astBindingNames])];
|
|
167
|
+
|
|
168
|
+
allBindingNames.forEach((bindingName) => {
|
|
169
|
+
if (!bindingName || isDynamicToken(bindingName)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const normalized = normalizeBindingName(bindingName);
|
|
174
|
+
const matchingAttrNode = astAttributes.find((attribute) => attribute.bindingName === bindingName);
|
|
175
|
+
const diagnosticColumn = Number.isInteger(matchingAttrNode?.range?.column)
|
|
176
|
+
? matchingAttrNode.range.column
|
|
177
|
+
: undefined;
|
|
178
|
+
const diagnosticEndColumn = Number.isInteger(matchingAttrNode?.range?.endColumn)
|
|
179
|
+
? matchingAttrNode.range.endColumn
|
|
180
|
+
: undefined;
|
|
181
|
+
|
|
182
|
+
if (normalized.sourceType === "legacy-prop") {
|
|
183
|
+
diagnostics.push({
|
|
184
|
+
code: "RTGL-CHECK-YAHTML-002",
|
|
185
|
+
severity: "error",
|
|
186
|
+
filePath,
|
|
187
|
+
line,
|
|
188
|
+
column: diagnosticColumn,
|
|
189
|
+
endLine: line,
|
|
190
|
+
endColumn: diagnosticEndColumn,
|
|
191
|
+
message: `${model.componentKey}: legacy '.${normalized.name}' binding is not supported. Use ':${normalized.name}'.`,
|
|
192
|
+
fix: {
|
|
193
|
+
kind: "line-regex-replace",
|
|
194
|
+
filePath,
|
|
195
|
+
line,
|
|
196
|
+
pattern: `\\.${normalized.name.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*=`,
|
|
197
|
+
replacement: `:${normalized.name}=`,
|
|
198
|
+
flags: "g",
|
|
199
|
+
description: `Convert '.${normalized.name}=' to ':${normalized.name}='.`,
|
|
200
|
+
safe: true,
|
|
201
|
+
confidence: 0.98,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (normalized.sourceType === "event") {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const attrName = normalized.name;
|
|
211
|
+
if (!attrName || isDynamicToken(attrName)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isGenericAllowed(attrName)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (isAllowedByContract({ attrName, contract })) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
diagnostics.push({
|
|
224
|
+
code: "RTGL-CHECK-YAHTML-003",
|
|
225
|
+
severity: "error",
|
|
226
|
+
filePath,
|
|
227
|
+
line,
|
|
228
|
+
column: diagnosticColumn,
|
|
229
|
+
endLine: line,
|
|
230
|
+
endColumn: diagnosticEndColumn,
|
|
231
|
+
message: `${model.componentKey}: unsupported attr/prop '${attrName}' on '${tagName}'.`,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return diagnostics;
|
|
238
|
+
};
|