@lewishowles/lint-config 0.2.0 → 0.4.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,143 @@
1
+ import { getDocumentationNode } from "../utils/documentation.js";
2
+ import { hasImmediateLineComment } from "../utils/source.js";
3
+
4
+ // The built-in APIs that require a preceding comment by default.
5
+ const builtInApis = new Set([
6
+ "onBeforeMount",
7
+ "onMounted",
8
+ "onBeforeUpdate",
9
+ "onUpdated",
10
+ "onBeforeUnmount",
11
+ "onUnmounted",
12
+ "onActivated",
13
+ "onDeactivated",
14
+ "onErrorCaptured",
15
+ "onRenderTracked",
16
+ "onRenderTriggered",
17
+ "onServerPrefetch",
18
+ "watch",
19
+ "watchEffect",
20
+ "watchPostEffect",
21
+ "watchSyncEffect",
22
+ "onClickOutside",
23
+ ]);
24
+
25
+ /**
26
+ * Return whether a call is the documented initializer of a variable
27
+ * declaration.
28
+ *
29
+ * @param {object} sourceCode
30
+ * The Oxlint source code object.
31
+ * @param {object} node
32
+ * The call expression node.
33
+ *
34
+ * @returns {boolean}
35
+ * Whether the call is directly initialized by a documented declaration.
36
+ */
37
+ function hasDocumentedVariableDeclaration(sourceCode, node) {
38
+ // The call's enclosing variable declarator, when there is one.
39
+ const declarator = node.parent;
40
+
41
+ if (declarator?.type !== "VariableDeclarator" || declarator.init !== node) {
42
+ return false;
43
+ }
44
+
45
+ // The declarator's enclosing variable declaration.
46
+ const declaration = declarator.parent;
47
+
48
+ if (declaration?.type !== "VariableDeclaration") {
49
+ return false;
50
+ }
51
+
52
+ // Resolves any export wrapper before checking for documentation.
53
+ const documentationNode = getDocumentationNode(declaration);
54
+
55
+ return hasImmediateLineComment(sourceCode, documentationNode);
56
+ }
57
+
58
+ /**
59
+ * Return the configured API names for the rule.
60
+ *
61
+ * @param {object} context
62
+ * The Oxlint rule context.
63
+ *
64
+ * @returns {Set<string>}
65
+ * The built-in and configured API names.
66
+ */
67
+ function getConfiguredApis(context) {
68
+ // The rule's resolved options for the file currently being visited.
69
+ const options = context.options?.[0];
70
+ // The project-configured API names to add to the built-in list.
71
+ const additionalApis = options?.additionalApis ?? [];
72
+
73
+ return new Set([...builtInApis, ...additionalApis]);
74
+ }
75
+
76
+ /**
77
+ * Create the configured API call comment rule.
78
+ *
79
+ * @returns {object}
80
+ * The Oxlint rule definition.
81
+ */
82
+ export default {
83
+ meta: {
84
+ docs: { description: "Require comments before configured API calls." },
85
+ type: "suggestion",
86
+ schema: [
87
+ {
88
+ type: "object",
89
+ properties: {
90
+ additionalApis: {
91
+ type: "array",
92
+ items: { type: "string" },
93
+ uniqueItems: true,
94
+ },
95
+ },
96
+ additionalProperties: false,
97
+ },
98
+ ],
99
+ defaultOptions: [{ additionalApis: [] }],
100
+ },
101
+
102
+ /**
103
+ * Create the rule's node visitors.
104
+ *
105
+ * @param {object} context
106
+ * The Oxlint rule context.
107
+ *
108
+ * @returns {object}
109
+ * The visitor functions for this rule.
110
+ */
111
+ createOnce(context) {
112
+ return {
113
+ /**
114
+ * Check a configured API call for a preceding comment.
115
+ *
116
+ * @param {object} node
117
+ * The call expression node.
118
+ */
119
+ CallExpression(node) {
120
+ // Read fresh for every call: createOnce's visitor is shared across every file
121
+ // in the run, so caching this at closure-creation time would freeze the first
122
+ // file's options.
123
+ const configuredApis = getConfiguredApis(context);
124
+
125
+ if (node.callee.type !== "Identifier" || !configuredApis.has(node.callee.name)) {
126
+ return;
127
+ }
128
+
129
+ if (
130
+ hasDocumentedVariableDeclaration(context.sourceCode, node) ||
131
+ hasImmediateLineComment(context.sourceCode, node)
132
+ ) {
133
+ return;
134
+ }
135
+
136
+ context.report({
137
+ message: "Configured API calls require an immediately preceding line comment.",
138
+ node,
139
+ });
140
+ },
141
+ };
142
+ },
143
+ };
@@ -0,0 +1,111 @@
1
+ import { isFunctionValue, reportFunctionDocumentation } from "../utils/documentation.js";
2
+
3
+ /**
4
+ * Return the declaration node that owns the documentation position.
5
+ *
6
+ * @param {object} node
7
+ * The function declaration node.
8
+ *
9
+ * @returns {object}
10
+ * The node immediately following the documentation block.
11
+ */
12
+ function getDocumentationNode(node) {
13
+ // Walks up through export wrappers to find the documented position.
14
+ let documentationNode = node;
15
+
16
+ while (
17
+ documentationNode.parent?.type === "ExportDefaultDeclaration" ||
18
+ documentationNode.parent?.type === "ExportNamedDeclaration"
19
+ ) {
20
+ documentationNode = documentationNode.parent;
21
+ }
22
+
23
+ return documentationNode;
24
+ }
25
+
26
+ /**
27
+ * Return whether an object property belongs to the outermost object literal.
28
+ *
29
+ * @param {object} node
30
+ * The property node to inspect.
31
+ *
32
+ * @returns {boolean}
33
+ * Whether the property is not nested inside another object literal.
34
+ */
35
+ function isFirstLevelObjectProperty(node) {
36
+ // Finds the object literal that owns the property.
37
+ const object = node.parent;
38
+
39
+ if (object?.type !== "ObjectExpression") {
40
+ return false;
41
+ }
42
+
43
+ return object.parent?.parent?.type !== "ObjectExpression";
44
+ }
45
+
46
+ /**
47
+ * Create the function-documentation rule.
48
+ *
49
+ * @returns {object}
50
+ * The Oxlint rule definition.
51
+ */
52
+ export default {
53
+ meta: {
54
+ docs: { description: "Require JSDoc documentation for named functions and methods." },
55
+ type: "suggestion",
56
+ },
57
+ /**
58
+ * Create the rule's node visitors.
59
+ *
60
+ * @param {object} context
61
+ * The Oxlint rule context.
62
+ *
63
+ * @returns {object}
64
+ * The visitor functions for this rule.
65
+ */
66
+ createOnce(context) {
67
+ return {
68
+ /**
69
+ * Check a named function declaration for documentation.
70
+ *
71
+ * @param {object} node
72
+ * The function declaration node.
73
+ */
74
+ FunctionDeclaration(node) {
75
+ if (node.id) {
76
+ reportFunctionDocumentation(context, getDocumentationNode(node), node);
77
+ }
78
+ },
79
+ /**
80
+ * Check a first-level object method for documentation.
81
+ *
82
+ * @param {object} node
83
+ * The property node.
84
+ */
85
+ Property(node) {
86
+ if (!isFirstLevelObjectProperty(node) || !isFunctionValue(node.value)) {
87
+ return;
88
+ }
89
+
90
+ reportFunctionDocumentation(context, node, node.value);
91
+ },
92
+ /**
93
+ * Check a const function variable for documentation.
94
+ *
95
+ * @param {object} node
96
+ * The variable declarator node.
97
+ */
98
+ VariableDeclarator(node) {
99
+ if (
100
+ node.id.type !== "Identifier" ||
101
+ node.parent?.kind !== "const" ||
102
+ !isFunctionValue(node.init)
103
+ ) {
104
+ return;
105
+ }
106
+
107
+ reportFunctionDocumentation(context, node.parent, node.init);
108
+ },
109
+ };
110
+ },
111
+ };
@@ -0,0 +1,70 @@
1
+ import { formatJSDocTagFormatting, hasTargetJSDocTag, isJSDoc } from "../utils/jsdoc.js";
2
+ import { getCommentText, replaceMinimalComment } from "../utils/source.js";
3
+
4
+ /**
5
+ * Create the JSDoc tag-formatting rule.
6
+ *
7
+ * @returns {object}
8
+ * The Oxlint rule definition.
9
+ */
10
+ export default {
11
+ meta: {
12
+ docs: { description: "Format Phase 1 JSDoc tag spacing and grouping." },
13
+ fixable: "code",
14
+ type: "layout",
15
+ },
16
+ /**
17
+ * Create the rule's node visitors.
18
+ *
19
+ * @param {object} context
20
+ * The Oxlint rule context.
21
+ *
22
+ * @returns {object}
23
+ * The visitor functions for this rule.
24
+ */
25
+ createOnce(context) {
26
+ return {
27
+ /**
28
+ * Format every JSDoc comment's tags in the file.
29
+ */
30
+ Program() {
31
+ for (const comment of context.sourceCode.getAllComments()) {
32
+ if (comment.type !== "Block") {
33
+ continue;
34
+ }
35
+
36
+ // The comment's raw source text.
37
+ const commentText = getCommentText(context.sourceCode, comment);
38
+
39
+ if (!isJSDoc(commentText) || !hasTargetJSDocTag(context.sourceCode, comment)) {
40
+ continue;
41
+ }
42
+
43
+ // The comment, with its tag spacing, order, and grouping normalised.
44
+ const formattedComment = formatJSDocTagFormatting(context.sourceCode, comment);
45
+
46
+ if (formattedComment === commentText) {
47
+ continue;
48
+ }
49
+
50
+ context.report({
51
+ /**
52
+ * Apply the formatted replacement to the comment.
53
+ *
54
+ * @param {object} fixer
55
+ * The Oxlint fixer.
56
+ *
57
+ * @returns {object}
58
+ * The fix to apply.
59
+ */
60
+ fix: (fixer) => {
61
+ return replaceMinimalComment(fixer, comment, commentText, formattedComment);
62
+ },
63
+ message: "JSDoc tags must use the configured spacing, order, and grouping.",
64
+ node: comment,
65
+ });
66
+ }
67
+ },
68
+ };
69
+ },
70
+ };
@@ -0,0 +1,86 @@
1
+ import {
2
+ getCommentText,
3
+ getLineCommentGroups,
4
+ getLineIndent,
5
+ getLineStart,
6
+ } from "../utils/source.js";
7
+
8
+ /**
9
+ * Create the line-comment alignment rule.
10
+ *
11
+ * @returns {object}
12
+ * The Oxlint rule definition.
13
+ */
14
+ export default {
15
+ meta: {
16
+ docs: { description: "Align wrapped line comments with their first marker." },
17
+ fixable: "code",
18
+ type: "layout",
19
+ },
20
+ /**
21
+ * Create the rule's node visitors.
22
+ *
23
+ * @param {object} context
24
+ * The Oxlint rule context.
25
+ *
26
+ * @returns {object}
27
+ * The visitor functions for this rule.
28
+ */
29
+ createOnce(context) {
30
+ return {
31
+ /**
32
+ * Align every wrapped line-comment group in the file.
33
+ */
34
+ Program() {
35
+ for (const commentGroup of getLineCommentGroups(context.sourceCode)) {
36
+ if (commentGroup.length < 2) {
37
+ continue;
38
+ }
39
+
40
+ // The group's leading comment's indentation.
41
+ const firstIndent = getLineIndent(context.sourceCode, commentGroup[0].range[0]);
42
+
43
+ if (firstIndent === null) {
44
+ continue;
45
+ }
46
+
47
+ // The reindentation fixes for the group's comments.
48
+ const fixes = [];
49
+
50
+ for (const comment of commentGroup) {
51
+ // The comment's current indentation.
52
+ const indentation = getLineIndent(context.sourceCode, comment.range[0]);
53
+
54
+ if (indentation === null || indentation === firstIndent) {
55
+ continue;
56
+ }
57
+
58
+ fixes.push({
59
+ range: [getLineStart(context.sourceCode, comment.range[0]), comment.range[1]],
60
+ text: `${firstIndent}${getCommentText(context.sourceCode, comment)}`,
61
+ });
62
+ }
63
+
64
+ if (fixes.length === 0) {
65
+ continue;
66
+ }
67
+
68
+ context.report({
69
+ /**
70
+ * Apply the group's alignment fixes.
71
+ *
72
+ * @param {object} fixer
73
+ * The Oxlint fixer.
74
+ *
75
+ * @returns {object[]}
76
+ * The fixes to apply.
77
+ */
78
+ fix: (fixer) => fixes.map((fix) => fixer.replaceTextRange(fix.range, fix.text)),
79
+ message: "Wrapped line comments must align with the first comment marker.",
80
+ node: commentGroup[0],
81
+ });
82
+ }
83
+ },
84
+ };
85
+ },
86
+ };
@@ -0,0 +1,159 @@
1
+ import { formatJSDocWrapping, isJSDoc } from "../utils/jsdoc.js";
2
+
3
+ import {
4
+ getCommentText,
5
+ getLineIndent,
6
+ getNewline,
7
+ isDirectiveComment,
8
+ replaceMinimalComment,
9
+ } from "../utils/source.js";
10
+
11
+ import { formatSentence, wrapWords } from "../utils/wrap.js";
12
+
13
+ // The line length this rule wraps comments to.
14
+ const maximumLineLength = 80;
15
+
16
+ /**
17
+ * Wrap a line comment to the configured maximum width.
18
+ *
19
+ * @param {object} sourceCode
20
+ * The Oxlint source code object.
21
+ * @param {object} comment
22
+ * The line comment token.
23
+ *
24
+ * @returns {string|null}
25
+ * The wrapped comment, or null when it is not a standalone comment.
26
+ */
27
+ function formatLineComment(sourceCode, comment) {
28
+ // The comment's current indentation.
29
+ const indentation = getLineIndent(sourceCode, comment.range[0]);
30
+
31
+ if (indentation === null) {
32
+ return null;
33
+ }
34
+
35
+ // The available width, allowing for the indent and "// " prefix.
36
+ const width = maximumLineLength - indentation.length - 3;
37
+ // The comment's undecorated text.
38
+ const text = comment.value.trim();
39
+
40
+ if (isDirectiveComment(comment)) {
41
+ return null;
42
+ }
43
+
44
+ if (text === "") {
45
+ return "//";
46
+ }
47
+
48
+ return wrapWords(text, Math.max(1, width))
49
+ .map((line, index) => `${index === 0 ? "" : indentation}// ${line}`)
50
+ .join(getNewline(sourceCode.text));
51
+ }
52
+
53
+ /**
54
+ * Wrap an ordinary block comment to the configured maximum width.
55
+ *
56
+ * @param {object} sourceCode
57
+ * The Oxlint source code object.
58
+ * @param {object} comment
59
+ * The block comment token.
60
+ *
61
+ * @returns {string|null}
62
+ * The wrapped comment, or null when it is not a standalone comment.
63
+ */
64
+ function formatBlockComment(sourceCode, comment) {
65
+ // The comment's current indentation.
66
+ const indentation = getLineIndent(sourceCode, comment.range[0]);
67
+
68
+ if (indentation === null) {
69
+ return null;
70
+ }
71
+
72
+ // The comment's raw source text.
73
+ const commentText = getCommentText(sourceCode, comment);
74
+ // The comment body, sentence-formatted.
75
+ const text = formatSentence(commentText.slice(2, -2).trim());
76
+ // The available width, allowing for the indent and " * " prefix.
77
+ const width = maximumLineLength - indentation.length - 3;
78
+ // The comment body, rewrapped to the available width.
79
+ const lines = wrapWords(text, Math.max(1, width));
80
+
81
+ return ["/*", ...lines.map((line) => `${indentation} * ${line}`), `${indentation} */`].join(
82
+ getNewline(sourceCode.text),
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Create the maximum-line-length rule.
88
+ *
89
+ * @returns {object}
90
+ * The Oxlint rule definition.
91
+ */
92
+ export default {
93
+ meta: {
94
+ docs: { description: "Wrap comments at 80 characters." },
95
+ fixable: "code",
96
+ type: "layout",
97
+ },
98
+ /**
99
+ * Create the rule's node visitors.
100
+ *
101
+ * @param {object} context
102
+ * The Oxlint rule context.
103
+ *
104
+ * @returns {object}
105
+ * The visitor functions for this rule.
106
+ */
107
+ createOnce(context) {
108
+ return {
109
+ /**
110
+ * Wrap every over-length comment in the file.
111
+ */
112
+ Program() {
113
+ for (const comment of context.sourceCode.getAllComments()) {
114
+ if (comment.type === "Shebang") {
115
+ continue;
116
+ }
117
+
118
+ // The comment's raw source text.
119
+ const commentText = getCommentText(context.sourceCode, comment);
120
+ // The comment's individual source lines.
121
+ const lines = commentText.split(/\r\n|\n|\r/);
122
+
123
+ if (!lines.some((line) => line.length > maximumLineLength)) {
124
+ continue;
125
+ }
126
+
127
+ // The comment, rewrapped using the formatter matching its type.
128
+ const formattedComment =
129
+ comment.type === "Line"
130
+ ? formatLineComment(context.sourceCode, comment)
131
+ : isJSDoc(commentText)
132
+ ? formatJSDocWrapping(context.sourceCode, comment)
133
+ : formatBlockComment(context.sourceCode, comment);
134
+
135
+ if (formattedComment === null || formattedComment === commentText) {
136
+ continue;
137
+ }
138
+
139
+ context.report({
140
+ /**
141
+ * Apply the wrapped replacement to the comment.
142
+ *
143
+ * @param {object} fixer
144
+ * The Oxlint fixer.
145
+ *
146
+ * @returns {object}
147
+ * The fix to apply.
148
+ */
149
+ fix: (fixer) => {
150
+ return replaceMinimalComment(fixer, comment, commentText, formattedComment);
151
+ },
152
+ message: "Comment exceeds 80 characters.",
153
+ node: comment,
154
+ });
155
+ }
156
+ },
157
+ };
158
+ },
159
+ };