@orion.ui/orion-linter 1.0.0
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/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/configs/eslint.config.mjs +256 -0
- package/dist/configs/stylelint.config.mjs +78 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +75 -0
- package/dist/rules/async-suffix.js +121 -0
- package/dist/rules/class-name-match-filename.js +34 -0
- package/dist/rules/default-props-are-static-readonly.js +48 -0
- package/dist/rules/events-are-in-camel-case.js +140 -0
- package/dist/rules/force-dynamic-vue-imports-in-router.js +80 -0
- package/dist/rules/force-dynamic-vue-imports-in-services.js +160 -0
- package/dist/rules/get-set-adjacent.js +154 -0
- package/dist/rules/get-set-one-liner.js +156 -0
- package/dist/rules/no-api-in-entity.js +32 -0
- package/dist/rules/no-api-in-setup.js +31 -0
- package/dist/rules/no-entity-in-service.js +31 -0
- package/dist/rules/no-export-type-in-ts.js +36 -0
- package/dist/rules/popables-are-readonly.js +52 -0
- package/dist/rules/private-property-if-only-in-template.js +192 -0
- package/dist/rules/state-are-private-readonly.js +89 -0
- package/dist/rules/template-refs-are-readonly.js +52 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +17 -0
- package/dist/utils.d.ts +29 -0
- package/dist/utils.js +66 -0
- package/package.json +66 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
'Ensure getters are grouped, get/set pairs are adjacent, and spacing is consistent.',
|
|
7
|
+
},
|
|
8
|
+
fixable: 'code',
|
|
9
|
+
messages: {
|
|
10
|
+
adjacent:
|
|
11
|
+
'Getter and setter must be ordered (get before set) and spaced correctly.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
create(context) {
|
|
16
|
+
const sourceCode = context.getSourceCode();
|
|
17
|
+
|
|
18
|
+
const isGetter = node =>
|
|
19
|
+
node.type === 'MethodDefinition' && node.kind === 'get';
|
|
20
|
+
|
|
21
|
+
const nameOf = (node) => {
|
|
22
|
+
if (!node || !node.key) return null;
|
|
23
|
+
if (node.key.type === 'Identifier') return node.key.name;
|
|
24
|
+
if (node.key.type === 'Literal') return String(node.key.value);
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const isOneLiner = (method) => {
|
|
29
|
+
const body = method?.value?.body;
|
|
30
|
+
if (!body || body.type !== 'BlockStatement') return false;
|
|
31
|
+
return body.loc.start.line === body.loc.end.line;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
ClassBody(node) {
|
|
36
|
+
const members = node.body;
|
|
37
|
+
|
|
38
|
+
const firstGetterIndex = members.findIndex(isGetter);
|
|
39
|
+
if (firstGetterIndex === -1) return;
|
|
40
|
+
|
|
41
|
+
const blockToReorder = members.slice(firstGetterIndex);
|
|
42
|
+
|
|
43
|
+
const text = sourceCode.getText();
|
|
44
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
45
|
+
|
|
46
|
+
// Separate into getters, pairs, others
|
|
47
|
+
const getters = [];
|
|
48
|
+
const pairs = [];
|
|
49
|
+
const others = [];
|
|
50
|
+
const setterMap = {};
|
|
51
|
+
|
|
52
|
+
blockToReorder.forEach((member) => {
|
|
53
|
+
if (member.type === 'MethodDefinition') {
|
|
54
|
+
const name = nameOf(member);
|
|
55
|
+
if (member.kind === 'get') {
|
|
56
|
+
const setter = setterMap[name];
|
|
57
|
+
if (setter) {
|
|
58
|
+
pairs.push(member, setter);
|
|
59
|
+
delete setterMap[name];
|
|
60
|
+
}
|
|
61
|
+
else getters.push(member);
|
|
62
|
+
}
|
|
63
|
+
else if (member.kind === 'set') {
|
|
64
|
+
const getter = getters.find(g => nameOf(g) === name);
|
|
65
|
+
if (getter) {
|
|
66
|
+
pairs.push(getter, member);
|
|
67
|
+
getters.splice(getters.indexOf(getter), 1);
|
|
68
|
+
}
|
|
69
|
+
else setterMap[name] = member;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
others.push(member);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
others.push(member);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Add remaining setters
|
|
81
|
+
Object.values(setterMap).forEach(s => others.push(s));
|
|
82
|
+
|
|
83
|
+
// Build text with correct indentation + spacing
|
|
84
|
+
const formatBlock = (list, isPairBlock = false) => {
|
|
85
|
+
if (!list.length) return '';
|
|
86
|
+
let text = sourceCode.getText(list[0]);
|
|
87
|
+
for (let i = 1; i < list.length; i++) {
|
|
88
|
+
const prev = list[i - 1];
|
|
89
|
+
const current = list[i];
|
|
90
|
+
|
|
91
|
+
if (isPairBlock) {
|
|
92
|
+
// Inside a pairs block:
|
|
93
|
+
// - Between a setter and the next getter (new pair): 2 line breaks
|
|
94
|
+
// - Between a getter and its setter: 2 line breaks if getter is multi-line, otherwise 1
|
|
95
|
+
if (prev.kind === 'set' && current.kind === 'get') {
|
|
96
|
+
text += eol + eol + '\t' + sourceCode.getText(current);
|
|
97
|
+
}
|
|
98
|
+
else if (prev.kind === 'get' && current.kind === 'set') {
|
|
99
|
+
const getterOneLiner = isOneLiner(prev);
|
|
100
|
+
const neededBlanks = getterOneLiner ? 1 : 2;
|
|
101
|
+
text += eol.repeat(neededBlanks) + '\t' + sourceCode.getText(current);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
text += eol + '\t' + sourceCode.getText(current);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// For standalone getters: normal behavior (1 or 2 breaks depending on one-liner)
|
|
109
|
+
const prevOneLiner = isOneLiner(prev);
|
|
110
|
+
const neededBlanks = prevOneLiner ? 1 : 2;
|
|
111
|
+
text += eol.repeat(neededBlanks) + '\t' + sourceCode.getText(current);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return text;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const firstToken = blockToReorder[0];
|
|
118
|
+
const lastToken = blockToReorder[blockToReorder.length - 1];
|
|
119
|
+
|
|
120
|
+
const newText = formatBlock(getters)
|
|
121
|
+
+ (getters.length && pairs.length ? eol + eol + '\t' : '')
|
|
122
|
+
+ formatBlock(pairs, true)
|
|
123
|
+
+ (others.length ? eol + eol + '\t' : '')
|
|
124
|
+
+ formatBlock(others);
|
|
125
|
+
|
|
126
|
+
// Compare structure + spacing (without trim to detect spacing differences)
|
|
127
|
+
const currentText = sourceCode.text.slice(
|
|
128
|
+
firstToken.range[0],
|
|
129
|
+
lastToken.range[1],
|
|
130
|
+
);
|
|
131
|
+
if (currentText === newText) return;
|
|
132
|
+
context.report({
|
|
133
|
+
node: getters[0] || pairs[0],
|
|
134
|
+
messageId: 'adjacent',
|
|
135
|
+
loc: {
|
|
136
|
+
start: {
|
|
137
|
+
line: (getters[0] || pairs[0])?.loc.start.line,
|
|
138
|
+
column: (getters[0] || pairs[0])?.loc.start.column,
|
|
139
|
+
},
|
|
140
|
+
end: {
|
|
141
|
+
line: (pairs[pairs.length - 1] || getters[getters.length - 1])?.loc.end.line,
|
|
142
|
+
column: (pairs[pairs.length - 1] || getters[getters.length - 1])?.loc.end.column,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
fix: fixer =>
|
|
146
|
+
fixer.replaceTextRange(
|
|
147
|
+
[firstToken.range[0], lastToken.range[1]],
|
|
148
|
+
newText,
|
|
149
|
+
),
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: { description: 'Enforce one-line getters and setters when they contain a single simple statement.' },
|
|
5
|
+
fixable: 'code',
|
|
6
|
+
messages: {
|
|
7
|
+
getterOneLiner: 'Getter body can be inlined to a one-liner.',
|
|
8
|
+
setterOneLiner: 'Setter body can be inlined to a one-liner.',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
create(context) {
|
|
13
|
+
const sourceCode = context.getSourceCode();
|
|
14
|
+
|
|
15
|
+
const hasInnerComments = (block) => {
|
|
16
|
+
if (!block) return false;
|
|
17
|
+
|
|
18
|
+
// 1) Dedicated API if available
|
|
19
|
+
if (typeof sourceCode.getCommentsInside === 'function') {
|
|
20
|
+
const comments = sourceCode.getCommentsInside(block) || [];
|
|
21
|
+
if (comments.length > 0) return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2) Tokens with includeComments
|
|
25
|
+
try {
|
|
26
|
+
const tokensWithComments = sourceCode.getTokens(block, { includeComments: true }) || [];
|
|
27
|
+
// Comment tokens usually have type 'Block' or 'Line'
|
|
28
|
+
if (tokensWithComments.some(t => t.type === 'Block' || t.type === 'Line')) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3) Fallback via ranges + getAllComments
|
|
37
|
+
const [start, end] = block.range || [];
|
|
38
|
+
const allComments = typeof sourceCode.getAllComments === 'function'
|
|
39
|
+
? (sourceCode.getAllComments() || [])
|
|
40
|
+
: [];
|
|
41
|
+
|
|
42
|
+
if (start != null && end != null && Array.isArray(allComments)) {
|
|
43
|
+
if (allComments.some(c => c.range && c.range[0] > start && c.range[1] < end)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4) Textual check inside braces
|
|
49
|
+
try {
|
|
50
|
+
const bodyText = sourceCode.getText(block) || '';
|
|
51
|
+
// Remove only the 1st and last braces
|
|
52
|
+
const innerText = bodyText.length >= 2 ? bodyText.slice(1, -1) : '';
|
|
53
|
+
// Detect // or /* */ even if parser doesn't expose comments
|
|
54
|
+
if (/\/\/|\/\*/.test(innerText)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// ignore
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 5) No comments detected
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
MethodDefinition(node) {
|
|
68
|
+
if (node.kind !== 'get' && node.kind !== 'set') return;
|
|
69
|
+
const fn = node.value;
|
|
70
|
+
const body = fn && fn.body;
|
|
71
|
+
if (!body || body.type !== 'BlockStatement') return;
|
|
72
|
+
const statements = body.body || [];
|
|
73
|
+
if (statements.length !== 1) return;
|
|
74
|
+
|
|
75
|
+
// Already a one-liner? -> ignore
|
|
76
|
+
const open = sourceCode.getFirstToken(body);
|
|
77
|
+
const close = sourceCode.getLastToken(body);
|
|
78
|
+
if (open && close) {
|
|
79
|
+
const between = sourceCode.text.slice(open.range[1], close.range[0]);
|
|
80
|
+
if (!/[\r\n]/.test(between)) return;
|
|
81
|
+
}
|
|
82
|
+
else if (body.loc && body.loc.start.line === body.loc.end.line) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Contains comments? -> ignore rule and don't propose fix
|
|
87
|
+
if (hasInnerComments(body)) return;
|
|
88
|
+
|
|
89
|
+
const stmt = statements[0];
|
|
90
|
+
|
|
91
|
+
if (node.kind === 'get') {
|
|
92
|
+
if (stmt.type !== 'ReturnStatement' || !stmt.argument) return;
|
|
93
|
+
|
|
94
|
+
// Check if returned expression is on a single line
|
|
95
|
+
const arg = stmt.argument;
|
|
96
|
+
if (arg.loc && arg.loc.start.line !== arg.loc.end.line) {
|
|
97
|
+
// Returned expression is on multiple lines -> ignore
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const argText = sourceCode.getText(stmt.argument);
|
|
102
|
+
|
|
103
|
+
// return of an object array -> don't propose fix
|
|
104
|
+
if (argText.includes('[') && argText.includes('{')) return;
|
|
105
|
+
context.report({
|
|
106
|
+
node,
|
|
107
|
+
messageId: 'getterOneLiner',
|
|
108
|
+
fix(fixer) {
|
|
109
|
+
if (hasInnerComments(body)) return null;
|
|
110
|
+
const newBody = `{ return ${argText}; }`;
|
|
111
|
+
const openBrace = sourceCode.getFirstToken(body);
|
|
112
|
+
const prevToken = openBrace ? sourceCode.getTokenBefore(openBrace, { includeComments: false }) : null;
|
|
113
|
+
// Replace in one go: (single) space + block
|
|
114
|
+
if (prevToken && body.range) {
|
|
115
|
+
return fixer.replaceTextRange([prevToken.range[1], body.range[1]], ' ' + newBody);
|
|
116
|
+
}
|
|
117
|
+
return fixer.replaceText(body, newBody);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
if (stmt.type !== 'ExpressionStatement') return;
|
|
123
|
+
|
|
124
|
+
// Check if expression is on a single line
|
|
125
|
+
const expr = stmt.expression;
|
|
126
|
+
if (expr && expr.loc && expr.loc.start.line !== expr.loc.end.line) {
|
|
127
|
+
// Expression is on multiple lines -> ignore
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const stmtText = sourceCode.getText(stmt).replace(/\s*$/, '');
|
|
132
|
+
// set of an object array -> don't propose fix
|
|
133
|
+
if (stmtText.includes('[') && stmtText.includes('{')) return;
|
|
134
|
+
if (stmtText.length > 120) return;
|
|
135
|
+
context.report({
|
|
136
|
+
node,
|
|
137
|
+
messageId: 'setterOneLiner',
|
|
138
|
+
fix(fixer) {
|
|
139
|
+
if (hasInnerComments(body)) return null;
|
|
140
|
+
const hasSemi = /;\s*$/.test(stmtText);
|
|
141
|
+
const finalStmt = hasSemi ? stmtText : `${stmtText};`;
|
|
142
|
+
const newBody = `{ ${finalStmt} }`;
|
|
143
|
+
const openBrace = sourceCode.getFirstToken(body);
|
|
144
|
+
const prevToken = openBrace ? sourceCode.getTokenBefore(openBrace, { includeComments: false }) : null;
|
|
145
|
+
// Replace in one go: (single) space + block
|
|
146
|
+
if (prevToken && body.range) {
|
|
147
|
+
return fixer.replaceTextRange([prevToken.range[1], body.range[1]], ' ' + newBody);
|
|
148
|
+
}
|
|
149
|
+
return fixer.replaceText(body, newBody);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: { description: `check that no API calls are made from *Entity.ts` },
|
|
5
|
+
},
|
|
6
|
+
|
|
7
|
+
create: function (context) {
|
|
8
|
+
return {
|
|
9
|
+
ImportDeclaration(node) {
|
|
10
|
+
const fileName = context.getPhysicalFilename().split('/').reverse()[0];
|
|
11
|
+
const fileIsEntity = /Entity\.ts$/.test(fileName);
|
|
12
|
+
|
|
13
|
+
if (!fileIsEntity) return;
|
|
14
|
+
|
|
15
|
+
// const entityName = fileName.replace('Entity.ts', '');
|
|
16
|
+
const importValue = node.source.value;
|
|
17
|
+
const importIsApi = /\/api\//.test(importValue);
|
|
18
|
+
|
|
19
|
+
if (!importIsApi) return;
|
|
20
|
+
|
|
21
|
+
const apiName = importValue.split('/').reverse()[0];
|
|
22
|
+
|
|
23
|
+
context.report({
|
|
24
|
+
node,
|
|
25
|
+
message: `Oops !
|
|
26
|
+
We should not use any API in *Entity.ts.
|
|
27
|
+
Use \`${apiName}\` in corresponding Service or create it.`,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: { description: `check that no API calls are made from *Setup(Service)?.ts` },
|
|
5
|
+
},
|
|
6
|
+
|
|
7
|
+
create: function (context) {
|
|
8
|
+
return {
|
|
9
|
+
ImportDeclaration(node) {
|
|
10
|
+
const fileName = context.getPhysicalFilename().split('/').reverse()[0];
|
|
11
|
+
const fileIsEntity = /Setup(Service)?\.ts$/.test(fileName);
|
|
12
|
+
|
|
13
|
+
if (!fileIsEntity) return;
|
|
14
|
+
|
|
15
|
+
const importValue = node.source.value;
|
|
16
|
+
const importIsApi = /\/api\//.test(importValue);
|
|
17
|
+
|
|
18
|
+
if (!importIsApi) return;
|
|
19
|
+
|
|
20
|
+
const apiName = importValue.split('/').reverse()[0];
|
|
21
|
+
|
|
22
|
+
context.report({
|
|
23
|
+
node,
|
|
24
|
+
message: `Oops !
|
|
25
|
+
We should not use any API in Setup.
|
|
26
|
+
Use \`${apiName}\` in corresponding Service or create it.`,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: { description: `check that no Entity are imported in *Service.ts (except for typing)` },
|
|
5
|
+
},
|
|
6
|
+
|
|
7
|
+
create: function (context) {
|
|
8
|
+
return {
|
|
9
|
+
ImportDeclaration(node) {
|
|
10
|
+
const fileName = context.getPhysicalFilename().split('/').reverse()[0];
|
|
11
|
+
const fileIsService = /(?<!Setup)Service\.ts$/.test(fileName);
|
|
12
|
+
|
|
13
|
+
if (!fileIsService) return;
|
|
14
|
+
|
|
15
|
+
// const serviceName = fileName.replace('Service.ts', '');
|
|
16
|
+
|
|
17
|
+
const importValue = node.source.value;
|
|
18
|
+
const importIsEntity = /\/entity\//.test(importValue);
|
|
19
|
+
|
|
20
|
+
if (!importIsEntity || node.source.parent.importKind === 'type') return;
|
|
21
|
+
|
|
22
|
+
context.report({
|
|
23
|
+
node,
|
|
24
|
+
message: `Oops !
|
|
25
|
+
We should not import any Entity in *Service.ts to avoid circular references.
|
|
26
|
+
If it's a type import, replace 'import' with 'import type'.`,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: { description: `check that no types are exported in simple .ts files (place them in shims-*.d.ts)` },
|
|
5
|
+
},
|
|
6
|
+
|
|
7
|
+
create: function (context) {
|
|
8
|
+
return {
|
|
9
|
+
ExportNamedDeclaration(node) {
|
|
10
|
+
const fileName = context.getPhysicalFilename().split('/').reverse()[0];
|
|
11
|
+
const fileIsDts = /\.d\.ts$/.test(fileName);
|
|
12
|
+
|
|
13
|
+
if (fileIsDts) return;
|
|
14
|
+
if (!node.declaration) return;
|
|
15
|
+
if (node.declaration.id?.name.endsWith('Emits')) return;
|
|
16
|
+
if (node.declaration.id?.name.endsWith('Props')) return;
|
|
17
|
+
// if (/^T[A-Z]+/.test(node.declaration.id?.name)) return;
|
|
18
|
+
|
|
19
|
+
const isTsTypeExport = node.declaration.type === 'TSTypeAliasDeclaration';
|
|
20
|
+
|
|
21
|
+
if (!isTsTypeExport) return;
|
|
22
|
+
|
|
23
|
+
const exportName = node.declaration.id.name;
|
|
24
|
+
|
|
25
|
+
context.report({
|
|
26
|
+
node,
|
|
27
|
+
message: `Oops !
|
|
28
|
+
We should declare non-Emits types in shims-*.d.ts to avoid types import all around the application.
|
|
29
|
+
Move \`${exportName}\` type declaration in either :
|
|
30
|
+
• src/shims-armado.d.ts (types métiers)
|
|
31
|
+
• src/shims-app.d.ts (types globaux)`,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function getClassProperties(classNode) {
|
|
2
|
+
return classNode.body.body.filter(x => x.type === 'PropertyDefinition');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: { description: `check that popables declared in Setup are readonly` },
|
|
9
|
+
fixable: 'code',
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
create: function (context) {
|
|
13
|
+
const sourceCode = context.sourceCode;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
ClassDeclaration(node) {
|
|
17
|
+
function getRefsDeclarations(classNode) {
|
|
18
|
+
const popableRegex = /^\w*\??:\s*Orion(Aside|Modal)/;
|
|
19
|
+
return getClassProperties(classNode)
|
|
20
|
+
.filter(propertyNode => popableRegex.test(sourceCode.getText(propertyNode)))
|
|
21
|
+
.map(propertyNode => ({
|
|
22
|
+
node: propertyNode,
|
|
23
|
+
name: propertyNode.key.name,
|
|
24
|
+
readonly: propertyNode.readonly,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getRefsDeclarations(node).forEach((propertyNode) => {
|
|
29
|
+
const messages = [];
|
|
30
|
+
const nameIsMissingUnderscore = propertyNode.name.charAt(0) !== '_';
|
|
31
|
+
|
|
32
|
+
if (!nameIsMissingUnderscore && propertyNode.readonly) return;
|
|
33
|
+
|
|
34
|
+
if (nameIsMissingUnderscore) messages.push(`Popable reference name should begin with '_'.`);
|
|
35
|
+
if (!propertyNode.readonly) messages.push(`Popable reference should be \`readonly\`.`);
|
|
36
|
+
|
|
37
|
+
context.report({
|
|
38
|
+
node: propertyNode.node,
|
|
39
|
+
message: `Oops !
|
|
40
|
+
${messages.join('\n')}`,
|
|
41
|
+
fix: (fixer) => {
|
|
42
|
+
return fixer.replaceTextRange(
|
|
43
|
+
propertyNode.node.range,
|
|
44
|
+
sourceCode.getText(propertyNode.node).replace(/^((readonly)?\s?_*)(.*)/, 'readonly _$3'),
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'suggestion',
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
'Ensure that properties only used in the setup are private',
|
|
10
|
+
},
|
|
11
|
+
fixable: 'code',
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
create(context) {
|
|
15
|
+
const currentFileName = context.getFilename();
|
|
16
|
+
const index = currentFileName.endsWith('SetupService.ts')
|
|
17
|
+
? currentFileName.indexOf('SetupService.ts')
|
|
18
|
+
: currentFileName.indexOf('Setup.ts');
|
|
19
|
+
const vueFileName = currentFileName.substring(0, index) + '.vue';
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(vueFileName)) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const vueSourceFile = fs.readFileSync(vueFileName, 'utf8');
|
|
26
|
+
const sourceCode = context.getSourceCode();
|
|
27
|
+
|
|
28
|
+
// Function to check if a property is declared in the parent class
|
|
29
|
+
// Returns { isAbstract: boolean, existsInParent: boolean }
|
|
30
|
+
const checkPropertyInParent = (propertyName, node) => {
|
|
31
|
+
// Get parent class if it exists
|
|
32
|
+
if (!node.parent || !node.parent.superClass) {
|
|
33
|
+
return { isAbstract: false, existsInParent: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Handle generic classes: BaseClass<T> -> extract just BaseClass
|
|
37
|
+
const superClassName = node.parent.superClass.name || (node.parent.superClass.callee && node.parent.superClass.callee.name);
|
|
38
|
+
if (!superClassName) {
|
|
39
|
+
return { isAbstract: false, existsInParent: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Look for parent class import
|
|
43
|
+
const imports = sourceCode.ast.body.filter(n => n.type === 'ImportDeclaration');
|
|
44
|
+
let parentFilePath = null;
|
|
45
|
+
|
|
46
|
+
for (const imp of imports) {
|
|
47
|
+
const defaultImport = imp.specifiers.find(s => s.type === 'ImportDefaultSpecifier');
|
|
48
|
+
if (defaultImport && defaultImport.local.name === superClassName) {
|
|
49
|
+
// Get path of imported file
|
|
50
|
+
const importPath = imp.source.value;
|
|
51
|
+
const currentDir = currentFileName.substring(0, currentFileName.lastIndexOf('/'));
|
|
52
|
+
|
|
53
|
+
// Resolve relative path - normalize path
|
|
54
|
+
if (importPath.startsWith('./') || importPath.startsWith('../')) {
|
|
55
|
+
parentFilePath = path.resolve(currentDir, importPath + '.ts');
|
|
56
|
+
}
|
|
57
|
+
else if (importPath.startsWith('@/')) {
|
|
58
|
+
// Handle @ alias pointing to src/
|
|
59
|
+
const srcDir = currentFileName.substring(0, currentFileName.indexOf('/src/') + 4);
|
|
60
|
+
const relativePath = importPath.substring(2); // remove '@/'
|
|
61
|
+
parentFilePath = path.join(srcDir, relativePath + '.ts');
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!parentFilePath || !fs.existsSync(parentFilePath)) {
|
|
68
|
+
return { isAbstract: false, existsInParent: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Recursive function to check property throughout the hierarchy
|
|
72
|
+
const checkInFile = (filePath, propName) => {
|
|
73
|
+
try {
|
|
74
|
+
const parentSource = fs.readFileSync(filePath, 'utf8');
|
|
75
|
+
|
|
76
|
+
// Check if it's abstract
|
|
77
|
+
// Regex handling: abstract filterService, abstract get listItemType(), etc.
|
|
78
|
+
const abstractRegex = new RegExp(`abstract\\s+(public\\s+|protected\\s+|private\\s+)?(get\\s+|readonly\\s+)?${propName}\\s*[;:(]`, 'gm');
|
|
79
|
+
if (abstractRegex.test(parentSource)) {
|
|
80
|
+
return { isAbstract: true, existsInParent: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if property/method exists (public, protected or readonly)
|
|
84
|
+
// Patterns: get propName(), propName =, propName:, readonly propName
|
|
85
|
+
// Includes access modifiers: public, protected, private
|
|
86
|
+
// Pattern starts with whitespace or line start to avoid false positives
|
|
87
|
+
const existsRegex = new RegExp(`(^|\\s)(public\\s+|protected\\s+|private\\s+)?(get\\s+|readonly\\s+)?${propName}\\s*[=:(]`, 'gm');
|
|
88
|
+
if (existsRegex.test(parentSource)) {
|
|
89
|
+
return { isAbstract: false, existsInParent: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// If not found, check parent class of this file
|
|
93
|
+
// Regex must match extends after generics closure > or after class name
|
|
94
|
+
// To avoid matching generic constraints like "T extends SomeType"
|
|
95
|
+
const extendsMatch = parentSource.match(/(?:>\s+|\bclass\s+\w+(?:<[^>]+>)?\s+)extends\s+(\w+)/);
|
|
96
|
+
if (extendsMatch) {
|
|
97
|
+
const parentClassName = extendsMatch[1];
|
|
98
|
+
const parentImportMatch = parentSource.match(new RegExp(`import\\s+${parentClassName}\\s+from\\s+['"]([^'"]+)['"]`));
|
|
99
|
+
if (parentImportMatch) {
|
|
100
|
+
const parentImportPath = parentImportMatch[1];
|
|
101
|
+
const parentDir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
102
|
+
let grandParentFilePath;
|
|
103
|
+
|
|
104
|
+
if (parentImportPath.startsWith('./') || parentImportPath.startsWith('../')) {
|
|
105
|
+
grandParentFilePath = path.resolve(parentDir, parentImportPath + '.ts');
|
|
106
|
+
}
|
|
107
|
+
else if (parentImportPath.startsWith('@/')) {
|
|
108
|
+
// Handle @ alias pointing to src/
|
|
109
|
+
const srcDir = filePath.substring(0, filePath.indexOf('/src/') + 4);
|
|
110
|
+
const relativePath = parentImportPath.substring(2); // remove '@/'
|
|
111
|
+
grandParentFilePath = path.join(srcDir, relativePath + '.ts');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (grandParentFilePath && fs.existsSync(grandParentFilePath)) {
|
|
115
|
+
return checkInFile(grandParentFilePath, propName);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { isAbstract: false, existsInParent: false };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return { isAbstract: false, existsInParent: false };
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return checkInFile(parentFilePath, propertyName);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
ClassBody(node) {
|
|
132
|
+
const members = node.body;
|
|
133
|
+
|
|
134
|
+
const properties = [];
|
|
135
|
+
|
|
136
|
+
for (const member of members) {
|
|
137
|
+
// Handle PropertyDefinition (properties) and MethodDefinition (methods/getters)
|
|
138
|
+
if (member.type === 'PropertyDefinition' || member.type === 'MethodDefinition') {
|
|
139
|
+
if (member.key.type === 'Identifier') {
|
|
140
|
+
// ignore defaultProps, state, constructor
|
|
141
|
+
if (['defaultProps', 'state', 'constructor'].includes(member.key.name)) continue;
|
|
142
|
+
|
|
143
|
+
properties.push({
|
|
144
|
+
name: member.key.name,
|
|
145
|
+
isPrivate: member.accessibility === 'private' || member.accessibility === 'protected',
|
|
146
|
+
inError: false,
|
|
147
|
+
member: member,
|
|
148
|
+
type: member.type,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const prop of properties) {
|
|
155
|
+
const parentCheck = checkPropertyInParent(prop.name, node);
|
|
156
|
+
|
|
157
|
+
// Ignore if property/method is abstract in parent class
|
|
158
|
+
if (parentCheck.isAbstract) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Ignore if property/method already exists (non-private) in parent class
|
|
163
|
+
// Because we cannot make a parent's public property private
|
|
164
|
+
if (parentCheck.existsInParent) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!vueSourceFile.includes(`setup.${prop.name}`) && !prop.isPrivate) {
|
|
169
|
+
prop.inError = true;
|
|
170
|
+
const itemType = prop.type === 'MethodDefinition' ? 'Method' : 'Property';
|
|
171
|
+
context.report({
|
|
172
|
+
message: `Oops! ${itemType} "${prop.name}" should be private as not used in associated .vue file`,
|
|
173
|
+
loc: {
|
|
174
|
+
start: prop.member.loc.start,
|
|
175
|
+
end: prop.member.loc.end,
|
|
176
|
+
},
|
|
177
|
+
fix: (fixer) => {
|
|
178
|
+
// If member has decorators, insert "private" after last decorator
|
|
179
|
+
if (prop.member.decorators && prop.member.decorators.length > 0) {
|
|
180
|
+
const lastDecorator = prop.member.decorators[prop.member.decorators.length - 1];
|
|
181
|
+
return fixer.insertTextAfter(lastDecorator, ' private');
|
|
182
|
+
}
|
|
183
|
+
// Otherwise, insert "private" before the member
|
|
184
|
+
return fixer.insertTextBefore(prop.member, 'private ');
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
};
|