@nextcloud/eslint-config 9.0.0-rc.1 → 9.0.0-rc.10
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/CHANGELOG.md +114 -42
- package/README.md +85 -31
- package/dist/configs/codeStyle.d.ts +1 -1
- package/dist/configs/codeStyle.js +27 -29
- package/dist/configs/documentation.js +71 -38
- package/dist/configs/filesystem.js +10 -3
- package/dist/configs/imports.d.ts +1 -1
- package/dist/configs/imports.js +15 -18
- package/dist/configs/javascript.js +2 -2
- package/dist/configs/typescript.js +8 -1
- package/dist/configs/vue.js +36 -12
- package/dist/configs/vue2.js +2 -2
- package/dist/configs/vue3.js +24 -0
- package/dist/globs.d.ts +2 -0
- package/dist/globs.js +6 -2
- package/dist/index.d.ts +11 -7
- package/dist/index.js +4 -1
- package/dist/plugins/import-extensions/index.d.ts +15 -0
- package/dist/plugins/{nextcloud-vue → import-extensions}/index.js +4 -0
- package/dist/plugins/import-extensions/rules/ban-inline-type-imports.d.ts +7 -0
- package/dist/plugins/import-extensions/rules/ban-inline-type-imports.js +176 -0
- package/dist/plugins/import-extensions/rules/extensions.d.ts +11 -0
- package/dist/plugins/import-extensions/rules/extensions.js +143 -0
- package/dist/plugins/import-extensions/rules/index.d.ts +8 -0
- package/dist/plugins/import-extensions/rules/index.js +10 -0
- package/dist/plugins/nextcloud/rules/index.js +16 -4
- package/dist/plugins/nextcloud/rules/l10n-enforce-ellipsis.js +44 -0
- package/dist/plugins/{l10n/rules/non-breaking-space.js → nextcloud/rules/l10n-non-breaking-space.js} +11 -2
- package/dist/plugins/nextcloud/rules/{no-deprecations.js → no-deprecated-globals.js} +16 -3
- package/dist/plugins/nextcloud/rules/no-deprecated-library-exports.js +89 -0
- package/dist/plugins/nextcloud/rules/no-deprecated-library-props.d.ts +41 -0
- package/dist/plugins/nextcloud/rules/no-deprecated-library-props.js +429 -0
- package/dist/plugins/nextcloud/rules/{no-removed-apis.js → no-removed-globals.js} +53 -2
- package/dist/plugins/nextcloud/utils/lib-version-parser.d.ts +18 -0
- package/dist/plugins/nextcloud/utils/lib-version-parser.js +49 -0
- package/dist/plugins/nextcloud/utils/vue-template-visitor.d.ts +26 -0
- package/dist/plugins/nextcloud/utils/vue-template-visitor.js +38 -0
- package/dist/utils.d.ts +1 -12
- package/package.json +31 -25
- package/dist/plugins/l10n/index.d.ts +0 -10
- package/dist/plugins/l10n/index.js +0 -17
- package/dist/plugins/l10n/rules/enforce-ellipsis.js +0 -32
- package/dist/plugins/nextcloud-vue/index.d.ts +0 -8
- package/dist/plugins/nextcloud-vue/rules/index.d.ts +0 -6
- package/dist/plugins/nextcloud-vue/rules/index.js +0 -4
- package/dist/plugins/nextcloud-vue/rules/no-deprecated-exports.js +0 -44
- package/dist/plugins/nextcloud-vue/utils/lib-version-parser.d.ts +0 -33
- package/dist/plugins/nextcloud-vue/utils/lib-version-parser.js +0 -94
- /package/dist/plugins/{l10n/rules/enforce-ellipsis.d.ts → nextcloud/rules/l10n-enforce-ellipsis.d.ts} +0 -0
- /package/dist/plugins/{l10n/rules/non-breaking-space.d.ts → nextcloud/rules/l10n-non-breaking-space.d.ts} +0 -0
- /package/dist/plugins/{nextcloud-vue/rules/no-deprecated-exports.d.ts → nextcloud/rules/no-deprecated-globals.d.ts} +0 -0
- /package/dist/plugins/nextcloud/rules/{no-deprecations.d.ts → no-deprecated-library-exports.d.ts} +0 -0
- /package/dist/plugins/nextcloud/rules/{no-removed-apis.d.ts → no-removed-globals.d.ts} +0 -0
- /package/dist/plugins/nextcloud/utils/{version-parser.d.ts → app-version-parser.d.ts} +0 -0
- /package/dist/plugins/nextcloud/utils/{version-parser.js → app-version-parser.js} +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-FileCopyrightText: 2015 Ben Mosher
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
export const rule = {
|
|
7
|
+
meta: {
|
|
8
|
+
fixable: 'code',
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
dialects: ['typescript'],
|
|
12
|
+
description: 'Ban the use of inline type-only markers for named imports.',
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
preferTopLevel: 'Prefer using a top-level type-only import instead of inline type specifiers.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
ImportDeclaration(node) {
|
|
21
|
+
if (!('importKind' in node)
|
|
22
|
+
|| node.importKind === 'type'
|
|
23
|
+
// no specifiers (import {} from '') cannot have inline - so is valid
|
|
24
|
+
|| node.specifiers.length === 0
|
|
25
|
+
|| (node.specifiers.length === 1
|
|
26
|
+
// default imports are both "inline" and "top-level"
|
|
27
|
+
&& (node.specifiers[0].type === 'ImportDefaultSpecifier'
|
|
28
|
+
// namespace imports are both "inline" and "top-level"
|
|
29
|
+
|| node.specifiers[0].type === 'ImportNamespaceSpecifier'))) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const typeSpecifiers = [];
|
|
33
|
+
const valueSpecifiers = [];
|
|
34
|
+
let defaultSpecifier;
|
|
35
|
+
for (const specifier of node.specifiers) {
|
|
36
|
+
if (specifier.type === 'ImportDefaultSpecifier') {
|
|
37
|
+
defaultSpecifier = specifier;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!('importKind' in specifier) || !specifier.importKind) {
|
|
41
|
+
valueSpecifiers.push(specifier);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (specifier.importKind === 'type') {
|
|
45
|
+
typeSpecifiers.push(specifier);
|
|
46
|
+
}
|
|
47
|
+
else if (specifier.importKind === 'value') {
|
|
48
|
+
valueSpecifiers.push(specifier);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const typeImport = getImportText(node, context.sourceCode, typeSpecifiers);
|
|
52
|
+
const newImports = typeImport.trim();
|
|
53
|
+
if (typeSpecifiers.length === node.specifiers.length) {
|
|
54
|
+
// all specifiers have inline specifiers - so we replace the entire import
|
|
55
|
+
context.report({
|
|
56
|
+
node,
|
|
57
|
+
messageId: 'preferTopLevel',
|
|
58
|
+
fix(fixer) {
|
|
59
|
+
return fixer.replaceText(node, newImports);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// remove specific specifiers and insert new imports for them
|
|
65
|
+
for (const specifier of typeSpecifiers) {
|
|
66
|
+
context.report({
|
|
67
|
+
node: specifier,
|
|
68
|
+
messageId: 'preferTopLevel',
|
|
69
|
+
fix(fixer) {
|
|
70
|
+
const fixes = [];
|
|
71
|
+
// if there are no value specifiers, then the other report fixer will be called, not this one
|
|
72
|
+
if (valueSpecifiers.length > 0) {
|
|
73
|
+
// import { Value, type Type } from 'mod';
|
|
74
|
+
// we can just remove the type specifiers
|
|
75
|
+
removeSpecifiers(fixes, fixer, context.sourceCode, typeSpecifiers);
|
|
76
|
+
// make the import nicely formatted by also removing the trailing comma after the last value import
|
|
77
|
+
// eg
|
|
78
|
+
// import { Value, type Type } from 'mod';
|
|
79
|
+
// to
|
|
80
|
+
// import { Value } from 'mod';
|
|
81
|
+
// not
|
|
82
|
+
// import { Value, } from 'mod';
|
|
83
|
+
removeCommaAfterNode(fixes, fixer, context.sourceCode, valueSpecifiers[valueSpecifiers.length - 1]);
|
|
84
|
+
}
|
|
85
|
+
else if (defaultSpecifier) {
|
|
86
|
+
// import Default, { type Type } from 'mod';
|
|
87
|
+
// remove the entire curly block so we don't leave an empty one behind
|
|
88
|
+
// NOTE - the default specifier *must* be the first specifier always!
|
|
89
|
+
// so a comma exists that we also have to clean up or else it's bad syntax
|
|
90
|
+
const comma = context.sourceCode.getTokenAfter(defaultSpecifier, isComma);
|
|
91
|
+
const closingBrace = context.sourceCode.getTokenAfter(node.specifiers[node.specifiers.length - 1], (token) => token.type === 'Punctuator' && token.value === '}');
|
|
92
|
+
if (comma && closingBrace) {
|
|
93
|
+
fixes.push(fixer.removeRange([
|
|
94
|
+
comma.range[0],
|
|
95
|
+
closingBrace.range[1],
|
|
96
|
+
]));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// insert the new imports after the old declaration
|
|
100
|
+
return fixes.concat(fixer.insertTextAfter(node, `\n${newImports}`));
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Check if the given token is a comma
|
|
111
|
+
*
|
|
112
|
+
* @param token - The token to check
|
|
113
|
+
*/
|
|
114
|
+
function isComma(token) {
|
|
115
|
+
return token.type === 'Punctuator' && token.value === ',';
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Remove the given specifiers from the import declaration, along with any trailing commas if necessary.
|
|
119
|
+
*
|
|
120
|
+
* @param fixes - The array to which the generated fixes will be added
|
|
121
|
+
* @param fixer - The fixer object used to create the fixes
|
|
122
|
+
* @param sourceCode - The source code object used to analyze the code and find tokens
|
|
123
|
+
* @param specifiers - The specifiers to remove from the import declaration
|
|
124
|
+
*/
|
|
125
|
+
function removeSpecifiers(fixes, fixer, sourceCode, specifiers) {
|
|
126
|
+
for (const specifier of specifiers) {
|
|
127
|
+
removeCommaAfterNode(fixes, fixer, sourceCode, specifier);
|
|
128
|
+
fixes.push(fixer.remove(specifier));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Remove the trailing comma
|
|
133
|
+
*
|
|
134
|
+
* @param fixes - The array to which the generated fixes will be added
|
|
135
|
+
* @param fixer - The fixer object used to create the fixes
|
|
136
|
+
* @param sourceCode - The source code object used to analyze the code and find tokens
|
|
137
|
+
* @param node - The node after which to check for a comma and remove it if exists
|
|
138
|
+
*/
|
|
139
|
+
function removeCommaAfterNode(fixes, fixer, sourceCode, node) {
|
|
140
|
+
const token = sourceCode.getTokenAfter(node);
|
|
141
|
+
if (token && isComma(token)) {
|
|
142
|
+
const nextToken = sourceCode.getTokenAfter(token);
|
|
143
|
+
// get the empty space to remove double whitespace after removing the comma
|
|
144
|
+
const emptySpace = sourceCode.text
|
|
145
|
+
.slice(token.range[1], nextToken?.range[0] ?? token.range[1])
|
|
146
|
+
.match(/^[ \t]*[\n\r]*/)?.[0] ?? '';
|
|
147
|
+
if (emptySpace) {
|
|
148
|
+
fixes.push(fixer.removeRange([token.range[0], token.range[1] + emptySpace.length]));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
fixes.push(fixer.remove(token));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get the text for a new top-level type-only import based on the given inline type specifiers.
|
|
157
|
+
*
|
|
158
|
+
* @param node - The original import declaration node containing the inline type specifiers
|
|
159
|
+
* @param sourceCode - The source code object used to analyze the code and find tokens
|
|
160
|
+
* @param specifiers - The inline type specifiers for which to generate the new import text
|
|
161
|
+
*/
|
|
162
|
+
function getImportText(node, sourceCode, specifiers) {
|
|
163
|
+
const sourceString = sourceCode.getText(node.source);
|
|
164
|
+
if (specifiers.length === 0) {
|
|
165
|
+
return '';
|
|
166
|
+
}
|
|
167
|
+
const names = specifiers.map((s) => {
|
|
168
|
+
const name = 'name' in s.imported ? s.imported.name : s.imported.value;
|
|
169
|
+
if (name === s.local.name) {
|
|
170
|
+
return name;
|
|
171
|
+
}
|
|
172
|
+
return `${name} as ${s.local.name}`;
|
|
173
|
+
});
|
|
174
|
+
// insert a fresh top-level import
|
|
175
|
+
return `import type {${names.join(', ')}} from ${sourceString};`;
|
|
176
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import type { Rule } from 'eslint';
|
|
6
|
+
/**
|
|
7
|
+
* helper for unit tests
|
|
8
|
+
*/
|
|
9
|
+
export declare function clearCache(): void;
|
|
10
|
+
export declare const rule: Rule.RuleModule;
|
|
11
|
+
export default rule;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, opendirSync, statSync } from 'fs';
|
|
6
|
+
import { basename, dirname, extname, join, resolve } from 'path';
|
|
7
|
+
// we use this module scope map to cache results for resolving the file extensions
|
|
8
|
+
// in larger projects its likely the same files are imported so we cache "absolute resolved import without extension" -> array of possible extensions
|
|
9
|
+
const fsCache = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* helper for unit tests
|
|
12
|
+
*/
|
|
13
|
+
export function clearCache() {
|
|
14
|
+
fsCache.clear();
|
|
15
|
+
}
|
|
16
|
+
export const rule = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
hasSuggestions: true,
|
|
20
|
+
docs: {
|
|
21
|
+
description: 'Ensure all relative imports and exports have a file extension',
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
missingExtension: 'This relative {{ type }} should have a file extension.',
|
|
25
|
+
recommendedMissingExtension: 'The relative {{ type }} should probably have the file extension "{{ extension }}".',
|
|
26
|
+
applySuggestedExtension: 'Add the "{{ extension }}" to the {{ type }}.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
create(context) {
|
|
30
|
+
/**
|
|
31
|
+
* @param node - The ESTree node representing the source
|
|
32
|
+
* @param isImport - is this an import or export
|
|
33
|
+
*/
|
|
34
|
+
function handleImportExport(node, isImport = true) {
|
|
35
|
+
const value = String(node.value);
|
|
36
|
+
if (!value.includes('/')) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// get rid of query like ?raw
|
|
40
|
+
const [text] = value.split('?', 2);
|
|
41
|
+
if (text.match(/\.[a-z0-9]+$/i)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const type = isImport ? 'import' : 'export';
|
|
45
|
+
// check custom paths - we cannot fix though
|
|
46
|
+
if (text.startsWith('~/')) {
|
|
47
|
+
context.report({
|
|
48
|
+
node,
|
|
49
|
+
messageId: 'missingExtension',
|
|
50
|
+
data: { type },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!text.match(/^\.\.?\//)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const contextPath = dirname(context.physicalFilename);
|
|
57
|
+
const relativePath = resolve(contextPath, text);
|
|
58
|
+
const absolutePath = resolve(context.cwd, relativePath);
|
|
59
|
+
if (!fsCache.has(absolutePath)) {
|
|
60
|
+
let resolvedPath = relativePath;
|
|
61
|
+
if (existsSync(resolvedPath)) {
|
|
62
|
+
if (statSync(resolvedPath).isDirectory) {
|
|
63
|
+
resolvedPath = join(resolvedPath, 'index');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// weird extensionless file
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const resolvedDirectoryPath = dirname(resolvedPath);
|
|
71
|
+
if (existsSync(resolvedDirectoryPath)) {
|
|
72
|
+
const filename = basename(resolvedPath);
|
|
73
|
+
const resolvedDir = opendirSync(resolvedDirectoryPath);
|
|
74
|
+
const extensions = [];
|
|
75
|
+
try {
|
|
76
|
+
let entry;
|
|
77
|
+
while ((entry = resolvedDir.readSync()) !== null) {
|
|
78
|
+
const extension = extname(entry.name);
|
|
79
|
+
if (extension && `${filename}${extension}` === entry.name) {
|
|
80
|
+
extensions.push(extension);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
resolvedDir.close();
|
|
86
|
+
}
|
|
87
|
+
fsCache.set(absolutePath, extensions);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (fsCache.get(absolutePath)?.length) {
|
|
91
|
+
return context.report({
|
|
92
|
+
node,
|
|
93
|
+
messageId: fsCache.get(absolutePath).length === 1
|
|
94
|
+
? 'recommendedMissingExtension'
|
|
95
|
+
: 'missingExtension',
|
|
96
|
+
data: {
|
|
97
|
+
extension: fsCache.get(absolutePath)[0],
|
|
98
|
+
type,
|
|
99
|
+
},
|
|
100
|
+
suggest: fsCache.get(absolutePath).map((extension) => ({
|
|
101
|
+
messageId: 'applySuggestedExtension',
|
|
102
|
+
data: {
|
|
103
|
+
extension,
|
|
104
|
+
type,
|
|
105
|
+
},
|
|
106
|
+
fix(fixer) {
|
|
107
|
+
const range = [node.range[0], node.range[0] + 1 + text.length];
|
|
108
|
+
return fixer.insertTextAfterRange(range, extension);
|
|
109
|
+
},
|
|
110
|
+
})),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// no way to fix it
|
|
114
|
+
context.report({
|
|
115
|
+
node,
|
|
116
|
+
messageId: 'missingExtension',
|
|
117
|
+
data: {
|
|
118
|
+
type,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
ExportAllDeclaration(node) {
|
|
124
|
+
handleImportExport(node.source, false);
|
|
125
|
+
},
|
|
126
|
+
ExportNamedDeclaration(node) {
|
|
127
|
+
if (node.source) {
|
|
128
|
+
handleImportExport(node.source, false);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
ImportDeclaration(node) {
|
|
132
|
+
handleImportExport(node.source);
|
|
133
|
+
},
|
|
134
|
+
ImportExpression(node) {
|
|
135
|
+
if (node.source.type === 'Literal') {
|
|
136
|
+
handleImportExport(node.source);
|
|
137
|
+
}
|
|
138
|
+
// we cannot handle dynamic imports here
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
export default rule;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
export declare const rules: {
|
|
6
|
+
'ban-inline-type-imports': import("eslint").Rule.RuleModule;
|
|
7
|
+
extensions: import("eslint").Rule.RuleModule;
|
|
8
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import { rule as banInlineTypeImports } from "./ban-inline-type-imports.js";
|
|
6
|
+
import { rule as extensions } from "./extensions.js";
|
|
7
|
+
export const rules = {
|
|
8
|
+
'ban-inline-type-imports': banInlineTypeImports,
|
|
9
|
+
extensions,
|
|
10
|
+
};
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import l10nEnforceEllipsis from "./l10n-enforce-ellipsis.js";
|
|
6
|
+
import l10nNonBreakingSpace from "./l10n-non-breaking-space.js";
|
|
7
|
+
import noDeprecatedGlobals from "./no-deprecated-globals.js";
|
|
8
|
+
import noDeprecatedLibraryExports from "./no-deprecated-library-exports.js";
|
|
9
|
+
import noDeprecatedLibraryProps from "./no-deprecated-library-props.js";
|
|
10
|
+
import noRemovedGlobals from "./no-removed-globals.js";
|
|
3
11
|
export const rules = {
|
|
4
|
-
'
|
|
5
|
-
'
|
|
12
|
+
'l10n-enforce-ellipsis': l10nEnforceEllipsis,
|
|
13
|
+
'l10n-non-breaking-space': l10nNonBreakingSpace,
|
|
14
|
+
'no-deprecated-globals': noDeprecatedGlobals,
|
|
15
|
+
'no-removed-globals': noRemovedGlobals,
|
|
16
|
+
'no-deprecated-library-exports': noDeprecatedLibraryExports,
|
|
17
|
+
'no-deprecated-library-props': noDeprecatedLibraryProps,
|
|
6
18
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import { defineTemplateBodyVisitor } from "../utils/vue-template-visitor.js";
|
|
6
|
+
const defineRule = (r) => r;
|
|
7
|
+
export default defineRule({
|
|
8
|
+
meta: {
|
|
9
|
+
fixable: 'code',
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
schema: [],
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Enforce consistent usageof ellipsis instead of tripple dots',
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
shoudUseEllipsis: 'Translated strings should use ellipsis character instead of triple dots',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
const visitor = {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
if (node.callee.type !== 'Identifier'
|
|
23
|
+
|| (node.callee.name !== 't' && node.callee.name !== 'n')) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (const argument of node.arguments) {
|
|
27
|
+
if (argument.type !== 'Literal') {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (argument.raw?.match(/(?<=[^.])\.\.\.(?!\.)/)) {
|
|
31
|
+
context.report({
|
|
32
|
+
node,
|
|
33
|
+
messageId: 'shoudUseEllipsis',
|
|
34
|
+
fix(fixer) {
|
|
35
|
+
return fixer.replaceText(argument, argument.raw.replaceAll(/(?<=[^.])\.\.\.(?!\.)/g, '…'));
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return defineTemplateBodyVisitor(context, visitor, visitor);
|
|
43
|
+
},
|
|
44
|
+
});
|
package/dist/plugins/{l10n/rules/non-breaking-space.js → nextcloud/rules/l10n-non-breaking-space.js}
RENAMED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import { defineTemplateBodyVisitor } from "../utils/vue-template-visitor.js";
|
|
1
6
|
const defineRule = (r) => r;
|
|
2
7
|
export default defineRule({
|
|
3
8
|
meta: {
|
|
@@ -7,9 +12,12 @@ export default defineRule({
|
|
|
7
12
|
docs: {
|
|
8
13
|
description: 'Enforce non-breaking spaces before ellipsis',
|
|
9
14
|
},
|
|
15
|
+
messages: {
|
|
16
|
+
precedeWithNonbreakingSpace: 'Ellipsis must be preceded by non-breaking spaces',
|
|
17
|
+
},
|
|
10
18
|
},
|
|
11
19
|
create(context) {
|
|
12
|
-
|
|
20
|
+
const visitor = {
|
|
13
21
|
Literal(node) {
|
|
14
22
|
if (typeof node.value !== 'string') {
|
|
15
23
|
return;
|
|
@@ -18,7 +26,7 @@ export default defineRule({
|
|
|
18
26
|
if (matches && matches[1] !== ' ') {
|
|
19
27
|
context.report({
|
|
20
28
|
node,
|
|
21
|
-
|
|
29
|
+
messageId: 'precedeWithNonbreakingSpace',
|
|
22
30
|
fix(fixer) {
|
|
23
31
|
return fixer.replaceText(node, node.raw.replaceAll(/\s+…/g, ' …'));
|
|
24
32
|
},
|
|
@@ -26,5 +34,6 @@ export default defineRule({
|
|
|
26
34
|
}
|
|
27
35
|
},
|
|
28
36
|
};
|
|
37
|
+
return defineTemplateBodyVisitor(context, visitor, visitor);
|
|
29
38
|
},
|
|
30
39
|
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import { createVersionValidator } from "../utils/app-version-parser.js";
|
|
2
6
|
// ------------------------------------------------------------------------------
|
|
3
7
|
// Rule Definition
|
|
4
8
|
// ------------------------------------------------------------------------------
|
|
@@ -31,15 +35,18 @@ const global = {
|
|
|
31
35
|
relative_modified_date: '16.0.0',
|
|
32
36
|
};
|
|
33
37
|
const oc = {
|
|
38
|
+
AppConfig: '16.0.0',
|
|
34
39
|
L10n: '26.0.0',
|
|
40
|
+
SystemTags: '32.0.0',
|
|
35
41
|
_capabilities: '17.0.0',
|
|
36
42
|
addTranslations: '17.0.0',
|
|
37
43
|
basename: '18.0.0',
|
|
38
44
|
coreApps: '17.0.0',
|
|
39
45
|
currentUser: '19.0.0',
|
|
46
|
+
dialogs: '30.0.0',
|
|
40
47
|
dirname: '18.0.0',
|
|
41
48
|
encodePath: '18.0.0',
|
|
42
|
-
fileIsBlacklisted: '
|
|
49
|
+
fileIsBlacklisted: '18.0.0',
|
|
43
50
|
filePath: '19.0.0',
|
|
44
51
|
generateUrl: '19.0.0',
|
|
45
52
|
get: '19.0.0',
|
|
@@ -71,12 +78,18 @@ const ocNested = {
|
|
|
71
78
|
humanFileSize: '20.0.0',
|
|
72
79
|
relativeModifiedDate: '20.0.0',
|
|
73
80
|
},
|
|
81
|
+
dialogs: {
|
|
82
|
+
fileexists: '29.0.0',
|
|
83
|
+
},
|
|
84
|
+
config: {
|
|
85
|
+
blacklist_files_regex: '30.0.0',
|
|
86
|
+
forbidden_filename_characters: '30.0.0',
|
|
87
|
+
},
|
|
74
88
|
};
|
|
75
89
|
const rule = {
|
|
76
90
|
meta: {
|
|
77
91
|
docs: {
|
|
78
92
|
description: 'Deprecated Nextcloud APIs',
|
|
79
|
-
category: 'Nextcloud',
|
|
80
93
|
recommended: true,
|
|
81
94
|
},
|
|
82
95
|
// fixable: null or "code" or "whitespace"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import { createLibVersionValidator } from "../utils/lib-version-parser.js";
|
|
6
|
+
/*
|
|
7
|
+
Introduced in @nextcloud/vue v8.23.0
|
|
8
|
+
https://github.com/nextcloud-libraries/nextcloud-vue/pull/6385
|
|
9
|
+
*/
|
|
10
|
+
const rule = {
|
|
11
|
+
meta: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Deprecated @nextcloud/vue import syntax',
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
fixable: 'code',
|
|
17
|
+
messages: {
|
|
18
|
+
outdatedVueLibrary: 'Installed @nextcloud/vue library is outdated and does not support all reported errors. Install latest compatible version',
|
|
19
|
+
deprecatedDist: 'Import from "@nextcloud/vue/dist" is deprecated',
|
|
20
|
+
deprecatedMixin: 'Mixins are no longer recommended by Vue. Consider using available alternatives',
|
|
21
|
+
deprecatedNcSettingsInputText: 'NcSettingsInputText is deprecated. Consider using available alternatives',
|
|
22
|
+
deprecatedTooltip: 'Tooltip directive is deprecated. use native title attribute or NcPopover instead',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
const versionSatisfies = createLibVersionValidator(context);
|
|
27
|
+
const isVersionValidForDist = versionSatisfies('8.23.0');
|
|
28
|
+
const oldPattern = '@nextcloud/vue/dist/([^/]+)/([^/.]+)';
|
|
29
|
+
const mixinPattern = '@nextcloud/vue/mixins/([^/.]+)';
|
|
30
|
+
const isVersionValidForTooltip = versionSatisfies('8.25.0');
|
|
31
|
+
const tooltipPattern = '@nextcloud/vue/directives/Tooltip';
|
|
32
|
+
const isVersionValidForNcSettingsInputText = versionSatisfies('8.31.0');
|
|
33
|
+
const patternForNcSettingsInputText = '@nextcloud/vue/components/NcSettingsInputText';
|
|
34
|
+
return {
|
|
35
|
+
ImportDeclaration: function (node) {
|
|
36
|
+
const importPath = node.source.value;
|
|
37
|
+
const matchForNcSettingsInputText = importPath.match(new RegExp(patternForNcSettingsInputText));
|
|
38
|
+
if (matchForNcSettingsInputText) {
|
|
39
|
+
if (!isVersionValidForNcSettingsInputText) {
|
|
40
|
+
context.report({ node, messageId: 'outdatedVueLibrary' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
messageId: 'deprecatedNcSettingsInputText',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const mixinMatch = importPath.match(new RegExp(mixinPattern, 'i'));
|
|
49
|
+
if (mixinMatch) {
|
|
50
|
+
if (!isVersionValidForDist) {
|
|
51
|
+
context.report({ node, messageId: 'outdatedVueLibrary' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
context.report({
|
|
55
|
+
node,
|
|
56
|
+
messageId: 'deprecatedMixin',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const tooltipMatch = importPath.match(new RegExp(tooltipPattern));
|
|
60
|
+
if (tooltipMatch) {
|
|
61
|
+
if (!isVersionValidForTooltip) {
|
|
62
|
+
context.report({ node, messageId: 'outdatedVueLibrary' });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: 'deprecatedTooltip',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const match = importPath.match(new RegExp(oldPattern));
|
|
71
|
+
if (match) {
|
|
72
|
+
if (!isVersionValidForDist) {
|
|
73
|
+
context.report({ node, messageId: 'outdatedVueLibrary' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const newImportPath = `'@nextcloud/vue/${match[1].toLowerCase()}/${match[2]}'`;
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: 'deprecatedDist',
|
|
80
|
+
fix(fixer) {
|
|
81
|
+
return fixer.replaceText(node.source, newImportPath);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
export default rule;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import type { Rule } from 'eslint';
|
|
6
|
+
declare const _default: {
|
|
7
|
+
meta: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
type: "problem";
|
|
12
|
+
fixable: "code";
|
|
13
|
+
messages: {
|
|
14
|
+
outdatedVueLibrary: string;
|
|
15
|
+
useTypeInstead: string;
|
|
16
|
+
useVariantInstead: string;
|
|
17
|
+
useDisableSwipeForNavInstead: string;
|
|
18
|
+
useHideStatusInstead: string;
|
|
19
|
+
useVerboseStatusInstead: string;
|
|
20
|
+
useNoPlaceholderInstead: string;
|
|
21
|
+
useFormatInstead: string;
|
|
22
|
+
useLocaleInstead: string;
|
|
23
|
+
useTypeDateRangeInstead: string;
|
|
24
|
+
useNoCloseInstead: string;
|
|
25
|
+
useNoCloseOnClickOutsideInstead: string;
|
|
26
|
+
useDisableSwipeForModalInstead: string;
|
|
27
|
+
useNoFocusTrapInstead: string;
|
|
28
|
+
useKeepOpenInstead: string;
|
|
29
|
+
useNcSelectUsersInstead: string;
|
|
30
|
+
useArrowEndInstead: string;
|
|
31
|
+
removeAriaHidden: string;
|
|
32
|
+
removeLimitWidth: string;
|
|
33
|
+
removeExact: string;
|
|
34
|
+
useCloseButtonOutsideInstead: string;
|
|
35
|
+
useModelValueInsteadChecked: string;
|
|
36
|
+
useModelValueInsteadValue: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
create(context: Rule.RuleContext): Rule.RuleListener;
|
|
40
|
+
};
|
|
41
|
+
export default _default;
|