Skip to content

Commit d42ad0e

Browse files
committed
Add a #region comment rule
1 parent 7427aa9 commit d42ad0e

File tree

8 files changed

+160
-7
lines changed

8 files changed

+160
-7
lines changed

eslint.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export default tseslint.config(
111111
plugins: {
112112
import: importPlugin,
113113
jsdoc: jsdocPlugin,
114+
'@sourceacademy': saLintPlugin
114115
},
115116
rules: {
116117
'import/first': 'warn',
@@ -136,6 +137,8 @@ export default tseslint.config(
136137
'jsdoc/check-alignment': 'warn',
137138
'jsdoc/require-asterisk-prefix': 'warn',
138139

140+
'@sourceacademy/region-comment': 'error',
141+
139142
'@stylistic/brace-style': ['warn', '1tbs', { allowSingleLine: true }],
140143
'@stylistic/function-call-spacing': ['warn', 'never'],
141144
'@stylistic/function-paren-newline': ['warn', 'multiline-arguments'],
@@ -163,7 +166,6 @@ export default tseslint.config(
163166
},
164167
plugins: {
165168
'@typescript-eslint': tseslint.plugin,
166-
'@sourceacademy': saLintPlugin
167169
},
168170
rules: {
169171
'no-unused-vars': 'off', // Use the typescript eslint rule instead

lib/lintplugin/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ESLint } from 'eslint';
22
import * as configs from './configs';
3+
import regionComment from './rules/regionComment';
34
import tabType from './rules/tabType';
45
import collateTypeImports from './rules/typeimports';
56

@@ -8,7 +9,8 @@ const plugin: ESLint.Plugin = {
89
rules: {
910
// @ts-expect-error typescript-eslint rules are typed differently
1011
'collate-type-imports': collateTypeImports,
11-
'tab-type': tabType
12+
'tab-type': tabType,
13+
'region-comment': regionComment
1214
},
1315
configs: {
1416
'js/recommended': configs.jsConfig,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { RuleTester } from "eslint";
2+
import regionComment from "../regionComment";
3+
4+
describe('Test regionComment', () => {
5+
const tester = new RuleTester();
6+
tester.run(
7+
'region-comment',
8+
regionComment,
9+
{
10+
valid: [
11+
`
12+
// #region hi
13+
// #endregion hi
14+
`,
15+
`
16+
// #region 1
17+
// #region 2
18+
// #endregion 2
19+
// #endregion 1
20+
`,
21+
'// Some other comment',
22+
'// #regionnot',
23+
'/* #region block comment ignored */'
24+
],
25+
invalid: [{
26+
code: `
27+
// #region
28+
// #endregion
29+
`,
30+
errors: 2
31+
}, {
32+
code: '// #region 1',
33+
errors: 1
34+
}, {
35+
code: '// #endregion 1',
36+
errors: 1
37+
}, {
38+
code: `
39+
// #endregion 1
40+
// #region 1
41+
`,
42+
errors: 2
43+
}, {
44+
code: `
45+
// #region 1
46+
// #region 2
47+
// #endregion 1
48+
`,
49+
errors: 1
50+
}]
51+
}
52+
);
53+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { Rule } from 'eslint';
2+
import type { SourceLocation } from 'estree';
3+
4+
const RE = /^\s*#(region|endregion)(?:$|\s+(.*)$)/;
5+
6+
interface CommentInfo {
7+
type: 'start' | 'end'
8+
regionName?: string
9+
loc: SourceLocation
10+
}
11+
12+
const regionComment = {
13+
meta: {
14+
type: 'problem',
15+
docs: {
16+
description: 'Ensures that every region comment has a corresponding endregion comment'
17+
}
18+
},
19+
create: context => ({
20+
Program() {
21+
const lineComments = context.sourceCode
22+
.getAllComments()
23+
.reduce<CommentInfo[]>((res, comment) => {
24+
if (comment.type !== 'Line') return res;
25+
const match = RE.exec(comment.value);
26+
if (match == null) return res;
27+
28+
const [, marker, name] = match;
29+
30+
return [
31+
...res,
32+
{
33+
loc: comment.loc!,
34+
regionName: name?.trim(),
35+
type: marker === 'region' ? 'start' : 'end',
36+
}
37+
];
38+
}, []);
39+
40+
const missingRegionNames = lineComments.filter(({ regionName }) => regionName === undefined);
41+
if (missingRegionNames.length > 0) {
42+
for (const comment of missingRegionNames) {
43+
context.report({
44+
message: `Expected region name for #${comment.type === 'start' ? '' : 'end'}region comment`,
45+
loc: comment.loc
46+
});
47+
}
48+
}
49+
50+
function searchForClosing(parentComment: CommentInfo): number | null {
51+
let level = 1;
52+
for (let i = 0; i < lineComments.length; i++) {
53+
const commentInfo = lineComments[i];
54+
if (
55+
commentInfo.regionName !== undefined &&
56+
commentInfo.regionName === parentComment.regionName
57+
) {
58+
if (commentInfo.type === 'start') level++;
59+
else level--;
60+
}
61+
62+
if (level === 0) return i;
63+
}
64+
65+
return null;
66+
}
67+
68+
while (lineComments.length > 0) {
69+
const comment = lineComments.shift()!;
70+
if (comment.regionName === undefined) {
71+
continue;
72+
}
73+
74+
if (comment.type === 'end') {
75+
context.report({
76+
message: '#endregion comment missing #region',
77+
loc: comment.loc
78+
});
79+
continue;
80+
}
81+
82+
const endIndex = searchForClosing(comment);
83+
if (endIndex === null) {
84+
context.report({
85+
message: 'Missing #endregion for #region',
86+
loc: comment.loc
87+
});
88+
} else {
89+
lineComments.splice(endIndex, 1);
90+
}
91+
}
92+
}
93+
})
94+
} satisfies Rule.RuleModule;
95+
96+
export default regionComment;

lib/markdown-tree/vitest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// @ts-check
33

44
import { defineProject, mergeConfig } from 'vitest/config';
5-
import rootConfig from '../../vitest.config';
5+
import rootConfig from '../../vitest.config.js';
66

77
export default mergeConfig(
88
rootConfig,

lib/modules-lib/src/tabs/AnimationError.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { Icon } from '@blueprintjs/core';
22
import { IconNames } from '@blueprintjs/icons';
33

4-
interface Props {
4+
export interface AnimationErrorProps {
55
error: Error
66
}
77

88
/**
99
* React component for displaying errors related to animations
1010
*/
11-
export default function AnimationError({ error }: Props) {
11+
export default function AnimationError({ error }: AnimationErrorProps) {
1212
return <div style={{
1313
display: 'flex',
1414
flexDirection: 'column',

lib/modules-lib/src/tabs/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// (not that you should be able to)
1010

1111
export { default as AnimationCanvas, type AnimCanvasProps } from './AnimationCanvas';
12-
export { default as AnimationError } from './AnimationError';
12+
export { default as AnimationError, type AnimationErrorProps } from './AnimationError';
1313
export { default as AutoLoopSwitch, type AutoLoopSwitchProps } from './AutoLoopSwitch';
1414
export { default as ButtonComponent, type ButtonComponentProps } from './ButtonComponent';
1515
export * from './css_constants';

lib/modules-lib/vitest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Modules-lib test config
22
import react from '@vitejs/plugin-react';
33
import { defineProject, mergeConfig } from 'vitest/config';
4-
import rootConfig from '../../vitest.config';
4+
import rootConfig from '../../vitest.config.js';
55

66
export default mergeConfig(
77
rootConfig,

0 commit comments

Comments
 (0)