@gitlab/eslint-plugin 21.2.1 → 21.3.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,27 @@
1
+ # @gitlab/no-hardcoded-urls
2
+
3
+ We should not hardcode or construct URLs on the frontend. See https://docs.gitlab.com/development/urls_in_gitlab/ for more information.
4
+
5
+ ## Options
6
+
7
+ ```json
8
+ {
9
+ "@gitlab/vue-no-hardcoded-urls": [
10
+ "error",
11
+ {
12
+ "allowedKeys": ["path", "redirect"],
13
+ "allowedFunctions": ["helpPagePath"],
14
+ "allowedInterpolationVariables": [
15
+ "FORUM_URL",
16
+ "DOCS_URL",
17
+ "PROMO_URL",
18
+ "CONTRIBUTE_URL",
19
+ "DOCS_URL_IN_EE_DIR",
20
+ "CUSTOMERS_PORTAL_URL"
21
+ ],
22
+ "allowedPatterns": ["\\/api\\/:version"],
23
+ "disallowedObjectProperties": ["relative_url_root"]
24
+ }
25
+ ]
26
+ }
27
+ ```
@@ -0,0 +1,30 @@
1
+ # @gitlab/vue-no-hardcoded-urls
2
+
3
+ We should not hardcode or construct URLs on the frontend. See https://docs.gitlab.com/development/urls_in_gitlab/ for more information.
4
+
5
+ ## Options
6
+
7
+ Nothing
8
+
9
+ ```json
10
+ {
11
+ "@gitlab/vue-no-hardcoded-urls": [
12
+ "error",
13
+ {
14
+ "allowedVueComponents": ["help-page-link"],
15
+ "allowedKeys": ["path", "redirect"],
16
+ "allowedFunctions": ["helpPagePath"],
17
+ "allowedInterpolationVariables": [
18
+ "FORUM_URL",
19
+ "DOCS_URL",
20
+ "PROMO_URL",
21
+ "CONTRIBUTE_URL",
22
+ "DOCS_URL_IN_EE_DIR",
23
+ "CUSTOMERS_PORTAL_URL"
24
+ ],
25
+ "allowedPatterns": ["\\/api\\/:version"],
26
+ "disallowedObjectProperties": ["relative_url_root"]
27
+ }
28
+ ]
29
+ }
30
+ ```
package/docs/rules.md CHANGED
@@ -5,6 +5,7 @@
5
5
  Available rules:
6
6
 
7
7
  - [no-global-event-off](./rules/no-global-event-off.md): Throw an error when globally deregistering all event listeners.
8
+ - [no-hardcoded-urls](./rules/no-hardcoded-urls.md): Disallow hardcoded URLs
8
9
  - [no-runtime-template-compiler](./rules/no-runtime-template-compiler.md): Disallow components which rely on a runtime template compiler.
9
10
  - [require-i18n-strings](./rules/require-i18n-strings.md): Detect a string which has been hard coded and requires externalization.
10
11
  - [require-valid-i18n-helpers](./rules/require-valid-i18n-helpers.md): Enforces valid usage of translation helpers in JavaScript.
@@ -13,6 +14,7 @@ Available rules:
13
14
  - [vtu-no-explicit-wrapper-destroy](./rules/vtu-no-explicit-wrapper-destroy.md): Prevents redundant destroy calls and null asignment
14
15
  - [vtu-no-wrapper-vm](./rules/vtu-no-wrapper-vm.md): Prevent direct access to `vm` internals for `@vue/test-utils` wrappers.
15
16
  - [vue-no-data-toggle](./rules/vue-no-data-toggle.md): Restrict the use of `data-toggle` bootstrap behaviors within Vue templates
17
+ - [vue-no-hardcoded-urls](./rules/vue-no-hardcoded-urls.md): Disallow hardcoded URLs in Vue templates
16
18
  - [vue-no-new-non-primitive-in-template](./rules/vue-no-new-non-primitive-in-template.md): Prevents non-primitive values from being declared in templates
17
19
  - [vue-no-undef-apollo-properties](./rules/vue-no-undef-apollo-properties.md): Require Apollo query properties to be initialized in component data.
18
20
  - [vue-prefer-dollar-scopedslots](./rules/vue-prefer-dollar-scopedslots.md): Prefer $scopedSlots over $slots for Vue 2.x.
package/lefthook.yml CHANGED
@@ -1,7 +1,9 @@
1
1
  pre-commit:
2
2
  piped: true
3
+ fail_on_changes: always
3
4
  commands:
4
5
  update:
5
6
  run: yarn run update
6
7
  prettier:
7
- run: yarn run pretty-quick --staged
8
+ glob: '*.{js,mjs,json,yml,md}'
9
+ run: yarn prettier --write {staged_files}
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  module.exports = {
5
5
  rules: {
6
6
  'no-global-event-off': require('./rules/no-global-event-off.js'),
7
+ 'no-hardcoded-urls': require('./rules/no-hardcoded-urls.js'),
7
8
  'no-runtime-template-compiler': require('./rules/no-runtime-template-compiler.js'),
8
9
  'require-i18n-strings': require('./rules/require-i18n-strings.js'),
9
10
  'require-valid-i18n-helpers': require('./rules/require-valid-i18n-helpers.js'),
@@ -12,6 +13,7 @@ module.exports = {
12
13
  'vtu-no-explicit-wrapper-destroy': require('./rules/vtu-no-explicit-wrapper-destroy.js'),
13
14
  'vtu-no-wrapper-vm': require('./rules/vtu-no-wrapper-vm.js'),
14
15
  'vue-no-data-toggle': require('./rules/vue-no-data-toggle.js'),
16
+ 'vue-no-hardcoded-urls': require('./rules/vue-no-hardcoded-urls.js'),
15
17
  'vue-no-new-non-primitive-in-template': require('./rules/vue-no-new-non-primitive-in-template.js'),
16
18
  'vue-no-undef-apollo-properties': require('./rules/vue-no-undef-apollo-properties.js'),
17
19
  'vue-prefer-dollar-scopedslots': require('./rules/vue-prefer-dollar-scopedslots.js'),
@@ -25,6 +27,14 @@ module.exports = {
25
27
  },
26
28
  configs: {
27
29
  base: require('./configs/base.js'),
30
+ 'best-practices': require('./configs/base/best-practices.js'),
31
+ errors: require('./configs/base/errors.js'),
32
+ es6: require('./configs/base/es6.js'),
33
+ imports: require('./configs/base/imports.js'),
34
+ node: require('./configs/base/node.js'),
35
+ strict: require('./configs/base/strict.js'),
36
+ style: require('./configs/base/style.js'),
37
+ variables: require('./configs/base/variables.js'),
28
38
  default: require('./configs/default.js'),
29
39
  i18n: require('./configs/i18n.js'),
30
40
  jest: require('./configs/jest.js'),
@@ -0,0 +1,71 @@
1
+ // ------------------------------------------------------------------------------
2
+ // Requirements
3
+ // ------------------------------------------------------------------------------
4
+ const { DOCS_BASE_URL } = require('../constants');
5
+ const {
6
+ literalValidator,
7
+ templateLiteralValidator,
8
+ memberExpressionValidator,
9
+ } = require('../utils/hardcoded-urls');
10
+
11
+ // ------------------------------------------------------------------------------
12
+ // Rule Definition
13
+ // ------------------------------------------------------------------------------
14
+
15
+ module.exports = {
16
+ meta: {
17
+ type: 'error',
18
+ docs: {
19
+ description: 'Disallow hardcoded URLs',
20
+ category: 'maintainability',
21
+ url: DOCS_BASE_URL + '/no-hardcoded-urls.md',
22
+ },
23
+ schema: [
24
+ {
25
+ type: 'object',
26
+ properties: {
27
+ allowedKeys: {
28
+ type: 'array',
29
+ items: { type: 'string' },
30
+ default: [],
31
+ },
32
+ allowedFunctions: {
33
+ type: 'array',
34
+ items: { type: 'string' },
35
+ default: [],
36
+ },
37
+ allowedInterpolationVariables: {
38
+ type: 'array',
39
+ items: { type: 'string' },
40
+ default: [],
41
+ },
42
+ allowedPatterns: {
43
+ type: 'array',
44
+ items: { type: 'string' },
45
+ default: [],
46
+ },
47
+ disallowedObjectProperties: {
48
+ type: 'array',
49
+ items: { type: 'string' },
50
+ default: [],
51
+ },
52
+ },
53
+ additionalProperties: false,
54
+ },
55
+ ],
56
+ },
57
+
58
+ create(context) {
59
+ return {
60
+ Literal(node) {
61
+ literalValidator(context, node);
62
+ },
63
+ TemplateLiteral(node) {
64
+ templateLiteralValidator(context, node);
65
+ },
66
+ MemberExpression(node) {
67
+ memberExpressionValidator(context, node);
68
+ },
69
+ };
70
+ },
71
+ };
@@ -0,0 +1,77 @@
1
+ // ------------------------------------------------------------------------------
2
+ // Requirements
3
+ // ------------------------------------------------------------------------------
4
+ const { DOCS_BASE_URL } = require('../constants');
5
+ const { defineTemplateBodyVisitor } = require('../utils/index');
6
+ const {
7
+ literalValidator,
8
+ templateLiteralValidator,
9
+ memberExpressionValidator,
10
+ } = require('../utils/hardcoded-urls');
11
+
12
+ // ------------------------------------------------------------------------------
13
+ // Rule Definition
14
+ // ------------------------------------------------------------------------------
15
+
16
+ module.exports = {
17
+ meta: {
18
+ type: 'error',
19
+ docs: {
20
+ description: 'Disallow hardcoded URLs in Vue templates',
21
+ category: 'maintainability',
22
+ url: DOCS_BASE_URL + '/vue-no-hardcoded-urls.md',
23
+ },
24
+ schema: [
25
+ {
26
+ type: 'object',
27
+ properties: {
28
+ allowedVueComponents: {
29
+ type: 'array',
30
+ items: { type: 'string' },
31
+ default: [],
32
+ },
33
+ allowedKeys: {
34
+ type: 'array',
35
+ items: { type: 'string' },
36
+ default: [],
37
+ },
38
+ allowedFunctions: {
39
+ type: 'array',
40
+ items: { type: 'string' },
41
+ default: [],
42
+ },
43
+ allowedInterpolationVariables: {
44
+ type: 'array',
45
+ items: { type: 'string' },
46
+ default: [],
47
+ },
48
+ allowedPatterns: {
49
+ type: 'array',
50
+ items: { type: 'string' },
51
+ default: [],
52
+ },
53
+ disallowedObjectProperties: {
54
+ type: 'array',
55
+ items: { type: 'string' },
56
+ default: [],
57
+ },
58
+ },
59
+ additionalProperties: false,
60
+ },
61
+ ],
62
+ },
63
+
64
+ create(context) {
65
+ return defineTemplateBodyVisitor(context, {
66
+ VLiteral(node) {
67
+ literalValidator(context, node);
68
+ },
69
+ TemplateLiteral(node) {
70
+ templateLiteralValidator(context, node);
71
+ },
72
+ MemberExpression(node) {
73
+ memberExpressionValidator(context, node);
74
+ },
75
+ });
76
+ },
77
+ };
@@ -0,0 +1,127 @@
1
+ const ERROR_MESSAGE =
2
+ 'Avoid hardcoded URLs. See https://docs.gitlab.com/development/urls_in_gitlab/.';
3
+
4
+ function traverse(node, type) {
5
+ if (node.type === type) {
6
+ return node;
7
+ }
8
+
9
+ if (!node.parent) {
10
+ return node;
11
+ }
12
+
13
+ return traverse(node.parent, type);
14
+ }
15
+
16
+ function literalValidator(context, node) {
17
+ if (isAllowedNode(context, node)) {
18
+ return;
19
+ }
20
+
21
+ if (isHardCodedUrl(context, node.value)) {
22
+ context.report({
23
+ node,
24
+ message: ERROR_MESSAGE,
25
+ });
26
+ }
27
+ }
28
+
29
+ function templateLiteralValidator(context, node) {
30
+ if (isAllowedNode(context, node)) {
31
+ return;
32
+ }
33
+
34
+ if (isConstructedUrl(context, node)) {
35
+ context.report({
36
+ node,
37
+ message: ERROR_MESSAGE,
38
+ });
39
+ }
40
+ }
41
+
42
+ function memberExpressionValidator(context, node) {
43
+ const { disallowedObjectProperties = [] } = context.options[0] || {};
44
+
45
+ if (disallowedObjectProperties.includes(node.property?.name)) {
46
+ context.report({
47
+ node,
48
+ message: ERROR_MESSAGE,
49
+ });
50
+ }
51
+ }
52
+
53
+ function isAllowedNode(context, node) {
54
+ const { parent } = node;
55
+ const {
56
+ allowedKeys = [],
57
+ allowedFunctions = [],
58
+ allowedVueComponents = [],
59
+ } = context.options[0] || {};
60
+
61
+ if (
62
+ parent?.type === 'Property' &&
63
+ parent?.key?.type === 'Identifier' &&
64
+ allowedKeys.includes(parent?.key?.name)
65
+ ) {
66
+ return true;
67
+ }
68
+
69
+ if (
70
+ parent?.type === 'CallExpression' &&
71
+ parent?.callee?.type === 'Identifier' &&
72
+ allowedFunctions.includes(parent?.callee?.name)
73
+ ) {
74
+ return true;
75
+ }
76
+
77
+ const vElement = traverse(node, 'VElement');
78
+ if (vElement && allowedVueComponents.includes(vElement.name)) {
79
+ return true;
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Check if a string looks like a hardcoded URL path
87
+ */
88
+ function isHardCodedUrl(context, value) {
89
+ if (typeof value !== 'string') return false;
90
+
91
+ const { allowedPatterns = [] } = context.options[0] || {};
92
+
93
+ if (allowedPatterns.some((pattern) => new RegExp(pattern).test(value))) {
94
+ return false;
95
+ }
96
+
97
+ // Check for path-like patterns:
98
+ // Starts with / followed by word characters
99
+ const urlPathPattern = /^\/(?![#?])[\/\.a-zA-Z0-9_-]+/;
100
+
101
+ return urlPathPattern.test(value);
102
+ }
103
+
104
+ /**
105
+ * Check if template literal looks like URL construction
106
+ */
107
+ function isConstructedUrl(context, node) {
108
+ const { allowedInterpolationVariables = [] } = context.options[0] || {};
109
+
110
+ if (
111
+ node.expressions.some((expression) => allowedInterpolationVariables.includes(expression.name))
112
+ ) {
113
+ return false;
114
+ }
115
+
116
+ // Check if any quasi (string part) looks like a hard coded URL
117
+ const hasHardCodedParts = node.quasis.some((quasi) => isHardCodedUrl(context, quasi.value.raw));
118
+
119
+ return hasHardCodedParts;
120
+ }
121
+
122
+ module.exports = {
123
+ literalValidator,
124
+ templateLiteralValidator,
125
+ memberExpressionValidator,
126
+ ERROR_MESSAGE,
127
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/eslint-plugin",
3
- "version": "21.2.1",
3
+ "version": "21.3.0",
4
4
  "description": "GitLab package for our custom eslint rules",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -38,40 +38,18 @@
38
38
  "eslint-plugin-promise": "^7.0.0",
39
39
  "eslint-plugin-unicorn": "^55.0.0",
40
40
  "eslint-plugin-vue": "^9.27.0",
41
- "vue-eslint-parser": "^9.4.3",
42
- "lodash": "^4.17.21"
41
+ "lodash": "^4.17.21",
42
+ "vue-eslint-parser": "^9.4.3"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "eslint": "^8.57.0 || ^9.0.0"
46
46
  },
47
47
  "devDependencies": {
48
+ "@changesets/cli": "^2.29.8",
48
49
  "eslint": "^8.57.0",
49
50
  "glob": "^7.2.0",
50
51
  "jest": "^27.5.1",
51
52
  "prettier": "^2.6.1",
52
- "pretty-quick": "^3.1.3",
53
53
  "yarn-deduplicate": "^6.0.2"
54
- },
55
- "release": {
56
- "branches": [
57
- "main"
58
- ],
59
- "verifyConditions": [
60
- "@semantic-release/changelog",
61
- "@semantic-release/npm",
62
- "@semantic-release/git",
63
- "@semantic-release/gitlab"
64
- ],
65
- "prepare": [
66
- "@semantic-release/changelog",
67
- "@semantic-release/npm",
68
- "@semantic-release/git"
69
- ],
70
- "publish": [
71
- "@semantic-release/npm",
72
- "@semantic-release/gitlab"
73
- ],
74
- "success": false,
75
- "fail": false
76
54
  }
77
55
  }
@@ -0,0 +1,19 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ check_yarn_install_is_idempotent() {
5
+ yarn install
6
+ if ! git diff --exit-code yarn.lock; then
7
+ echo "Lock file is not stable. See above for diff." >&2
8
+ echo "To fix this, run the following and commit the result:" >&2
9
+ echo "yarn && yarn yarn-deduplicate --strategy fewer && yarn" >&2
10
+ exit 1
11
+ fi
12
+ }
13
+
14
+ # We need to sandwich yarn-deduplicate with yarn install calls.
15
+ # This is how Renovate does it:
16
+ # https://gitlab.com/gitlab-org/frontend/renovate-fork/-/blob/f7745bc34724f01e2d953cfb047c706d0e3f7fc4/lib/modules/manager/npm/post-update/yarn.ts#L251-254
17
+ check_yarn_install_is_idempotent
18
+ yarn run yarn-deduplicate --strategy fewer --list --fail
19
+ check_yarn_install_is_idempotent
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ yarn install --frozen-lockfile
5
+ yarn run update
6
+ if ! git diff --exit-code; then
7
+ echo "Files not up to date after editing rules."
8
+ echo "To fix this, run the following and commit the result:"
9
+ echo "yarn run update"
10
+ exit 1
11
+ fi