@alexgorbatchev/typescript-ai-policy 0.1.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.
Files changed (62) hide show
  1. package/README.md +223 -0
  2. package/package.json +60 -0
  3. package/src/oxfmt/createOxfmtConfig.ts +26 -0
  4. package/src/oxlint/assertNoRuleCollisions.ts +40 -0
  5. package/src/oxlint/createOxlintConfig.ts +161 -0
  6. package/src/oxlint/oxlint.config.ts +3 -0
  7. package/src/oxlint/plugin.ts +90 -0
  8. package/src/oxlint/rules/component-directory-file-convention.ts +65 -0
  9. package/src/oxlint/rules/component-file-contract.ts +328 -0
  10. package/src/oxlint/rules/component-file-location-convention.ts +43 -0
  11. package/src/oxlint/rules/component-file-naming-convention.ts +260 -0
  12. package/src/oxlint/rules/component-story-file-convention.ts +108 -0
  13. package/src/oxlint/rules/fixture-export-naming-convention.ts +72 -0
  14. package/src/oxlint/rules/fixture-export-type-contract.ts +264 -0
  15. package/src/oxlint/rules/fixture-file-contract.ts +91 -0
  16. package/src/oxlint/rules/fixture-import-path-convention.ts +125 -0
  17. package/src/oxlint/rules/helpers.ts +544 -0
  18. package/src/oxlint/rules/hook-export-location-convention.ts +169 -0
  19. package/src/oxlint/rules/hook-file-contract.ts +179 -0
  20. package/src/oxlint/rules/hook-file-naming-convention.ts +151 -0
  21. package/src/oxlint/rules/hook-test-file-convention.ts +60 -0
  22. package/src/oxlint/rules/hooks-directory-file-convention.ts +75 -0
  23. package/src/oxlint/rules/index-file-contract.ts +177 -0
  24. package/src/oxlint/rules/interface-naming-convention.ts +72 -0
  25. package/src/oxlint/rules/no-conditional-logic-in-tests.ts +53 -0
  26. package/src/oxlint/rules/no-fixture-exports-outside-fixture-entrypoint.ts +68 -0
  27. package/src/oxlint/rules/no-imports-from-tests-directory.ts +114 -0
  28. package/src/oxlint/rules/no-inline-fixture-bindings-in-tests.ts +54 -0
  29. package/src/oxlint/rules/no-inline-type-expressions.ts +169 -0
  30. package/src/oxlint/rules/no-local-type-declarations-in-fixture-files.ts +55 -0
  31. package/src/oxlint/rules/no-module-mocking.ts +85 -0
  32. package/src/oxlint/rules/no-non-running-tests.ts +72 -0
  33. package/src/oxlint/rules/no-react-create-element.ts +59 -0
  34. package/src/oxlint/rules/no-test-file-exports.ts +52 -0
  35. package/src/oxlint/rules/no-throw-in-tests.ts +40 -0
  36. package/src/oxlint/rules/no-type-exports-from-constants.ts +97 -0
  37. package/src/oxlint/rules/no-type-imports-from-constants.ts +73 -0
  38. package/src/oxlint/rules/no-value-exports-from-types.ts +115 -0
  39. package/src/oxlint/rules/require-component-root-testid.ts +547 -0
  40. package/src/oxlint/rules/require-template-indent.ts +83 -0
  41. package/src/oxlint/rules/single-fixture-entrypoint.ts +142 -0
  42. package/src/oxlint/rules/stories-directory-file-convention.ts +55 -0
  43. package/src/oxlint/rules/story-export-contract.ts +343 -0
  44. package/src/oxlint/rules/story-file-location-convention.ts +64 -0
  45. package/src/oxlint/rules/story-meta-type-annotation.ts +129 -0
  46. package/src/oxlint/rules/test-file-location-convention.ts +115 -0
  47. package/src/oxlint/rules/testid-naming-convention.ts +63 -0
  48. package/src/oxlint/rules/tests-directory-file-convention.ts +55 -0
  49. package/src/oxlint/rules/types.ts +45 -0
  50. package/src/semantic-fixes/applyFileChanges.ts +81 -0
  51. package/src/semantic-fixes/applySemanticFixes.ts +239 -0
  52. package/src/semantic-fixes/applyTextEdits.ts +164 -0
  53. package/src/semantic-fixes/backends/tsgo-lsp/TsgoLspClient.ts +439 -0
  54. package/src/semantic-fixes/backends/tsgo-lsp/createTsgoLspSemanticFixBackend.ts +251 -0
  55. package/src/semantic-fixes/providers/createInterfaceNamingConventionSemanticFixProvider.ts +132 -0
  56. package/src/semantic-fixes/providers/createTestFileLocationConventionSemanticFixProvider.ts +52 -0
  57. package/src/semantic-fixes/readMovedFileTextEdits.ts +150 -0
  58. package/src/semantic-fixes/readSemanticFixRuntimePaths.ts +38 -0
  59. package/src/semantic-fixes/runApplySemanticFixes.ts +120 -0
  60. package/src/semantic-fixes/runOxlintJson.ts +139 -0
  61. package/src/semantic-fixes/types.ts +163 -0
  62. package/src/shared/mergeConfig.ts +38 -0
@@ -0,0 +1,72 @@
1
+ import type { AstExpression, RuleModule } from "./types.ts";
2
+ import { isTestFile } from "./helpers.ts";
3
+
4
+ type CalleeMember = {
5
+ objectName: string;
6
+ propertyName: string;
7
+ };
8
+
9
+ const TEST_FUNCTION_NAMES = new Set(["describe", "it", "test"]);
10
+ const BLOCKED_TEST_MODIFIERS = new Set(["if", "skipIf", "todo", "todoIf"]);
11
+
12
+ function readCalleeMember(node: AstExpression): CalleeMember | null {
13
+ if (
14
+ node.type !== "MemberExpression" ||
15
+ node.computed ||
16
+ node.object.type !== "Identifier" ||
17
+ node.property.type !== "Identifier"
18
+ ) {
19
+ return null;
20
+ }
21
+
22
+ return {
23
+ objectName: node.object.name,
24
+ propertyName: node.property.name,
25
+ };
26
+ }
27
+
28
+ const noNonRunningTestsRule: RuleModule = {
29
+ meta: {
30
+ type: "problem" as const,
31
+ docs: {
32
+ description: "Ban non-running test modifiers that suppress or gate test execution",
33
+ },
34
+ schema: [],
35
+ messages: {
36
+ forbiddenModifier:
37
+ 'Remove "{{ fullName }}". Committed tests must always run, so use plain describe(...), it(...), or test(...) instead of gated, skipped, or todo variants.',
38
+ },
39
+ },
40
+ create(context) {
41
+ if (!isTestFile(context.filename)) {
42
+ return {};
43
+ }
44
+
45
+ return {
46
+ CallExpression(node) {
47
+ const calleeMember = readCalleeMember(node.callee);
48
+ if (!calleeMember) {
49
+ return;
50
+ }
51
+
52
+ if (!TEST_FUNCTION_NAMES.has(calleeMember.objectName)) {
53
+ return;
54
+ }
55
+
56
+ if (!BLOCKED_TEST_MODIFIERS.has(calleeMember.propertyName)) {
57
+ return;
58
+ }
59
+
60
+ context.report({
61
+ node,
62
+ messageId: "forbiddenModifier",
63
+ data: {
64
+ fullName: `${calleeMember.objectName}.${calleeMember.propertyName}`,
65
+ },
66
+ });
67
+ },
68
+ };
69
+ },
70
+ };
71
+
72
+ export default noNonRunningTestsRule;
@@ -0,0 +1,59 @@
1
+ import type { RuleModule } from "./types.ts";
2
+
3
+ const noReactCreateElementRule: RuleModule = {
4
+ meta: {
5
+ type: "problem" as const,
6
+ docs: {
7
+ description: "Ban React createElement in regular application code; use JSX instead",
8
+ },
9
+ schema: [],
10
+ messages: {
11
+ noImportedCreateElement:
12
+ 'Remove the createElement import from "react" and rewrite the rendered output as JSX syntax.',
13
+ noReactCreateElement: "Replace React.createElement(...) with equivalent JSX syntax.",
14
+ },
15
+ },
16
+ create(context) {
17
+ return {
18
+ ImportDeclaration(node) {
19
+ if (node.source?.value !== "react") {
20
+ return;
21
+ }
22
+
23
+ node.specifiers.forEach((specifier) => {
24
+ if (specifier.type !== "ImportSpecifier") {
25
+ return;
26
+ }
27
+
28
+ if (specifier.imported.type !== "Identifier" || specifier.imported.name !== "createElement") {
29
+ return;
30
+ }
31
+
32
+ context.report({
33
+ node: specifier,
34
+ messageId: "noImportedCreateElement",
35
+ });
36
+ });
37
+ },
38
+ CallExpression(node) {
39
+ if (
40
+ node.callee.type !== "MemberExpression" ||
41
+ node.callee.computed ||
42
+ node.callee.object.type !== "Identifier" ||
43
+ node.callee.object.name !== "React" ||
44
+ node.callee.property.type !== "Identifier" ||
45
+ node.callee.property.name !== "createElement"
46
+ ) {
47
+ return;
48
+ }
49
+
50
+ context.report({
51
+ node,
52
+ messageId: "noReactCreateElement",
53
+ });
54
+ },
55
+ };
56
+ },
57
+ };
58
+
59
+ export default noReactCreateElementRule;
@@ -0,0 +1,52 @@
1
+ import type { RuleModule } from "./types.ts";
2
+ import { getBaseName } from "./helpers.ts";
3
+
4
+ const TEST_FILE_NAME_PATTERN = /\.test\.tsx?$/u;
5
+
6
+ const noTestFileExportsRule: RuleModule = {
7
+ meta: {
8
+ type: "problem" as const,
9
+ docs: {
10
+ description: "Ban exports from .test.ts and .test.tsx files",
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ unexpectedTestExport:
15
+ "Remove this export from the test file. Move shared test code into helpers.ts, fixtures.ts, or fixtures/ instead.",
16
+ },
17
+ },
18
+ create(context) {
19
+ if (!TEST_FILE_NAME_PATTERN.test(getBaseName(context.filename))) {
20
+ return {};
21
+ }
22
+
23
+ return {
24
+ ExportAllDeclaration(node) {
25
+ context.report({
26
+ node,
27
+ messageId: "unexpectedTestExport",
28
+ });
29
+ },
30
+ ExportDefaultDeclaration(node) {
31
+ context.report({
32
+ node,
33
+ messageId: "unexpectedTestExport",
34
+ });
35
+ },
36
+ ExportNamedDeclaration(node) {
37
+ context.report({
38
+ node,
39
+ messageId: "unexpectedTestExport",
40
+ });
41
+ },
42
+ TSExportAssignment(node) {
43
+ context.report({
44
+ node,
45
+ messageId: "unexpectedTestExport",
46
+ });
47
+ },
48
+ };
49
+ },
50
+ };
51
+
52
+ export default noTestFileExportsRule;
@@ -0,0 +1,40 @@
1
+ import type { RuleModule } from "./types.ts";
2
+ import { isTestFile } from "./helpers.ts";
3
+
4
+ const noThrowInTestsRule: RuleModule = {
5
+ meta: {
6
+ type: "problem" as const,
7
+ docs: {
8
+ description: 'Ban "throw new Error(...)" inside committed test files',
9
+ },
10
+ schema: [],
11
+ messages: {
12
+ forbiddenThrowNewError:
13
+ 'Remove this "throw new Error(...)" from the test. Use assert(...) or assert.fail(...) from "node:assert" so failures stay explicit and deterministic.',
14
+ },
15
+ },
16
+ create(context) {
17
+ if (!isTestFile(context.filename)) {
18
+ return {};
19
+ }
20
+
21
+ return {
22
+ ThrowStatement(node) {
23
+ if (node.argument?.type !== "NewExpression") {
24
+ return;
25
+ }
26
+
27
+ if (node.argument.callee.type !== "Identifier" || node.argument.callee.name !== "Error") {
28
+ return;
29
+ }
30
+
31
+ context.report({
32
+ node,
33
+ messageId: "forbiddenThrowNewError",
34
+ });
35
+ },
36
+ };
37
+ },
38
+ };
39
+
40
+ export default noThrowInTestsRule;
@@ -0,0 +1,97 @@
1
+ import type { AstExportNamedDeclaration, AstExportSpecifier, RuleModule } from "./types.ts";
2
+ import { hasBaseName, readDeclarationIdentifierNames, readLiteralStringValue } from "./helpers.ts";
3
+
4
+ function readExportedSpecifierName(specifier: AstExportSpecifier): string {
5
+ if (specifier.exported.type === "Identifier") {
6
+ return specifier.exported.name;
7
+ }
8
+
9
+ return String(specifier.exported.value);
10
+ }
11
+
12
+ function isTypeExportSpecifier(specifier: AstExportSpecifier, exportDeclaration: AstExportNamedDeclaration): boolean {
13
+ return exportDeclaration.exportKind === "type" || specifier.exportKind === "type";
14
+ }
15
+
16
+ const noTypeExportsFromConstantsRule: RuleModule = {
17
+ meta: {
18
+ type: "problem" as const,
19
+ docs: {
20
+ description: 'Disallow exporting type-only API from files whose filename is "constants"',
21
+ },
22
+ schema: [],
23
+ messages: {
24
+ unexpectedTypeExport:
25
+ 'Move exported type "{{ name }}" out of this "constants" file and into a sibling "types.ts" module. "constants.ts" must export runtime values only.',
26
+ unexpectedTypeExportAll:
27
+ 'Do not re-export types from "{{ exportPath }}" through a "constants" file. Re-export them from a sibling "types.ts" module instead.',
28
+ },
29
+ },
30
+ create(context) {
31
+ if (!hasBaseName(context.filename, "constants")) {
32
+ return {};
33
+ }
34
+
35
+ return {
36
+ ExportAllDeclaration(node) {
37
+ if (node.exportKind !== "type") {
38
+ return;
39
+ }
40
+
41
+ context.report({
42
+ node,
43
+ messageId: "unexpectedTypeExportAll",
44
+ data: {
45
+ exportPath: readLiteralStringValue(node.source) ?? "this module",
46
+ },
47
+ });
48
+ },
49
+ ExportNamedDeclaration(node) {
50
+ if (node.declaration) {
51
+ if (node.exportKind !== "type") {
52
+ return;
53
+ }
54
+
55
+ const exportNames = readDeclarationIdentifierNames(node.declaration);
56
+ if (exportNames.length === 0) {
57
+ context.report({
58
+ node: node.declaration,
59
+ messageId: "unexpectedTypeExport",
60
+ data: {
61
+ name: "this binding",
62
+ },
63
+ });
64
+ return;
65
+ }
66
+
67
+ exportNames.forEach((name) => {
68
+ context.report({
69
+ node: node.declaration,
70
+ messageId: "unexpectedTypeExport",
71
+ data: {
72
+ name,
73
+ },
74
+ });
75
+ });
76
+ return;
77
+ }
78
+
79
+ node.specifiers.forEach((specifier) => {
80
+ if (!isTypeExportSpecifier(specifier, node)) {
81
+ return;
82
+ }
83
+
84
+ context.report({
85
+ node: specifier,
86
+ messageId: "unexpectedTypeExport",
87
+ data: {
88
+ name: readExportedSpecifierName(specifier),
89
+ },
90
+ });
91
+ });
92
+ },
93
+ };
94
+ },
95
+ };
96
+
97
+ export default noTypeExportsFromConstantsRule;
@@ -0,0 +1,73 @@
1
+ import type { AstImportClause, AstImportDeclaration, RuleModule } from "./types.ts";
2
+ import { hasBaseName, readLiteralStringValue } from "./helpers.ts";
3
+
4
+ function readImportBindingName(specifier: AstImportClause): string {
5
+ if (specifier.local?.type === "Identifier") {
6
+ return specifier.local.name;
7
+ }
8
+
9
+ return "this binding";
10
+ }
11
+
12
+ function isTypeImportSpecifier(specifier: AstImportClause, importDeclaration: AstImportDeclaration): boolean {
13
+ return (
14
+ importDeclaration.importKind === "type" || (specifier.type === "ImportSpecifier" && specifier.importKind === "type")
15
+ );
16
+ }
17
+
18
+ const noTypeImportsFromConstantsRule: RuleModule = {
19
+ meta: {
20
+ type: "problem" as const,
21
+ docs: {
22
+ description: 'Disallow type imports from modules whose filename is "constants"',
23
+ },
24
+ schema: [],
25
+ messages: {
26
+ unexpectedInlineTypeImport:
27
+ 'Do not reference "{{ importPath }}" through a type import. Move that exported type into a sibling "types.ts" module instead.',
28
+ unexpectedTypeImport:
29
+ 'Do not import type "{{ name }}" from "{{ importPath }}". Move that exported type into a sibling "types.ts" module instead.',
30
+ },
31
+ },
32
+ create(context) {
33
+ return {
34
+ ImportDeclaration(node) {
35
+ const importPath = readLiteralStringValue(node.source);
36
+ if (!importPath || !hasBaseName(importPath, "constants")) {
37
+ return;
38
+ }
39
+
40
+ node.specifiers.forEach((specifier) => {
41
+ if (!isTypeImportSpecifier(specifier, node)) {
42
+ return;
43
+ }
44
+
45
+ context.report({
46
+ node: specifier,
47
+ messageId: "unexpectedTypeImport",
48
+ data: {
49
+ name: readImportBindingName(specifier),
50
+ importPath,
51
+ },
52
+ });
53
+ });
54
+ },
55
+ TSImportType(node) {
56
+ const importPath = readLiteralStringValue(node.source);
57
+ if (!importPath || !hasBaseName(importPath, "constants")) {
58
+ return;
59
+ }
60
+
61
+ context.report({
62
+ node,
63
+ messageId: "unexpectedInlineTypeImport",
64
+ data: {
65
+ importPath,
66
+ },
67
+ });
68
+ },
69
+ };
70
+ },
71
+ };
72
+
73
+ export default noTypeImportsFromConstantsRule;
@@ -0,0 +1,115 @@
1
+ import type { AstExportNamedDeclaration, AstExportSpecifier, RuleModule } from "./types.ts";
2
+ import { hasBaseName, isTypeDeclaration, readDeclarationIdentifierNames, readLiteralStringValue } from "./helpers.ts";
3
+
4
+ function readExportedSpecifierName(specifier: AstExportSpecifier): string {
5
+ if (specifier.exported.type === "Identifier") {
6
+ return specifier.exported.name;
7
+ }
8
+
9
+ return String(specifier.exported.value);
10
+ }
11
+
12
+ function isTypeOnlyExportSpecifier(
13
+ specifier: AstExportSpecifier,
14
+ exportDeclaration: AstExportNamedDeclaration,
15
+ ): boolean {
16
+ return exportDeclaration.exportKind === "type" || specifier.exportKind === "type";
17
+ }
18
+
19
+ const noValueExportsFromTypesRule: RuleModule = {
20
+ meta: {
21
+ type: "problem" as const,
22
+ docs: {
23
+ description: 'Disallow value exports from files whose filename is "types"',
24
+ },
25
+ schema: [],
26
+ messages: {
27
+ unexpectedDefaultExport: 'Remove this default export from the "types" file. "types.ts" may export types only.',
28
+ unexpectedExportAssignment:
29
+ 'Remove this runtime export assignment from the "types" file. "types.ts" may export types only.',
30
+ unexpectedValueExport:
31
+ 'Move exported value "{{ name }}" out of this "types" file. "types.ts" may export types only; put runtime values in "constants.ts" or another implementation module instead.',
32
+ unexpectedValueExportAll:
33
+ 'Do not re-export runtime values from "{{ exportPath }}" through a "types" file. Re-export them from "constants.ts" or another implementation module instead.',
34
+ },
35
+ },
36
+ create(context) {
37
+ if (!hasBaseName(context.filename, "types")) {
38
+ return {};
39
+ }
40
+
41
+ return {
42
+ ExportAllDeclaration(node) {
43
+ if (node.exportKind === "type") {
44
+ return;
45
+ }
46
+
47
+ context.report({
48
+ node,
49
+ messageId: "unexpectedValueExportAll",
50
+ data: {
51
+ exportPath: readLiteralStringValue(node.source) ?? "this module",
52
+ },
53
+ });
54
+ },
55
+ ExportDefaultDeclaration(node) {
56
+ context.report({
57
+ node,
58
+ messageId: "unexpectedDefaultExport",
59
+ });
60
+ },
61
+ ExportNamedDeclaration(node) {
62
+ if (node.declaration) {
63
+ if (isTypeDeclaration(node.declaration)) {
64
+ return;
65
+ }
66
+
67
+ const exportNames = readDeclarationIdentifierNames(node.declaration);
68
+ if (exportNames.length === 0) {
69
+ context.report({
70
+ node: node.declaration,
71
+ messageId: "unexpectedValueExport",
72
+ data: {
73
+ name: "this binding",
74
+ },
75
+ });
76
+ return;
77
+ }
78
+
79
+ exportNames.forEach((name) => {
80
+ context.report({
81
+ node: node.declaration,
82
+ messageId: "unexpectedValueExport",
83
+ data: {
84
+ name,
85
+ },
86
+ });
87
+ });
88
+ return;
89
+ }
90
+
91
+ node.specifiers.forEach((specifier) => {
92
+ if (isTypeOnlyExportSpecifier(specifier, node)) {
93
+ return;
94
+ }
95
+
96
+ context.report({
97
+ node: specifier,
98
+ messageId: "unexpectedValueExport",
99
+ data: {
100
+ name: readExportedSpecifierName(specifier),
101
+ },
102
+ });
103
+ });
104
+ },
105
+ TSExportAssignment(node) {
106
+ context.report({
107
+ node,
108
+ messageId: "unexpectedExportAssignment",
109
+ });
110
+ },
111
+ };
112
+ },
113
+ };
114
+
115
+ export default noValueExportsFromTypesRule;