@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.
@@ -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
+ };