Skip to content

Commit 20858d7

Browse files
authored
Fix some bugs of adding missing rules (#205)
* include similar class names in the missing rule addition target * fix failure to add missing rule for named exports * add changelog * allow to add missing rules from non-component files
1 parent b59e64d commit 20858d7

6 files changed

Lines changed: 109 additions & 23 deletions

File tree

.changeset/fluffy-hairs-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@css-modules-kit/ts-plugin': patch
3+
---
4+
5+
fix: fix failure to add missing rule for named exports

.changeset/rare-signs-leave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@css-modules-kit/ts-plugin': patch
3+
---
4+
5+
fix: include similar class names in the missing rule addition target

.changeset/silent-bats-stick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@css-modules-kit/ts-plugin': minor
3+
---
4+
5+
feat: allow to add missing rules from non-component files

packages/ts-plugin/e2e/feature/code-fix.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import ts from 'typescript';
33
import { describe, expect, test } from 'vitest';
44
import {
55
CANNOT_FIND_NAME_ERROR_CODE,
6-
PROPERTY_DOES_NOT_EXIST_ERROR_CODE,
6+
PROPERTY_DOES_NOT_EXIST_ERROR_CODES,
77
} from '../../src/language-service/feature/code-fix.js';
88
import { createIFF } from '../test-util/fixture.js';
99
import { formatPath, launchTsserver, normalizeCodeFixActions } from '../test-util/tsserver.js';
@@ -88,7 +88,25 @@ describe('Get Code Fixes', async () => {
8888
file: iff.paths['a.tsx'],
8989
line: 3,
9090
offset: 11,
91-
errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODE],
91+
errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]],
92+
expected: [
93+
{
94+
fixName: 'fixMissingCSSRule',
95+
changes: [
96+
{
97+
fileName: formatPath(iff.paths['a.module.css']),
98+
textChanges: [{ start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: '\n.a_1 {\n \n}' }],
99+
},
100+
],
101+
},
102+
],
103+
},
104+
{
105+
name: 'styles.a_1',
106+
file: iff.paths['a.tsx'],
107+
line: 3,
108+
offset: 11,
109+
errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[1]],
92110
expected: [
93111
{
94112
fixName: 'fixMissingCSSRule',
@@ -106,7 +124,7 @@ describe('Get Code Fixes', async () => {
106124
file: iff.paths['a.tsx'],
107125
line: 4,
108126
offset: 12,
109-
errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODE],
127+
errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]],
110128
expected: [
111129
{
112130
fixName: 'fixMissingCSSRule',

packages/ts-plugin/e2e/named-exports.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import dedent from 'dedent';
33
import ts from 'typescript';
44
import { describe, expect, test } from 'vitest';
5+
import { PROPERTY_DOES_NOT_EXIST_ERROR_CODES } from '../src/language-service/feature/code-fix.js';
56
import { createIFF } from './test-util/fixture.js';
67
import {
78
formatPath,
@@ -442,4 +443,51 @@ describe('supports code fixes', async () => {
442443
expect(normalizeCodeFixActions(res.body!)).toStrictEqual(normalizeCodeFixActions(expected));
443444
});
444445
});
446+
test('supports adding missing CSS rules', async () => {
447+
const iff = await createIFF({
448+
'a.tsx': dedent`
449+
import * as styles from './a.module.css';
450+
styles.a_1;
451+
`,
452+
'a.module.css': '',
453+
'tsconfig.json': dedent`
454+
{
455+
"cmkOptions": {
456+
"namedExports": true
457+
}
458+
}
459+
`,
460+
});
461+
const tsserver = launchTsserver();
462+
await tsserver.sendUpdateOpen({
463+
openFiles: [{ file: iff.paths['tsconfig.json'] }],
464+
});
465+
const res = await tsserver.sendGetCodeFixes({
466+
errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]],
467+
file: iff.paths['a.tsx'],
468+
startLine: 2,
469+
startOffset: 8,
470+
endLine: 2,
471+
endOffset: 11,
472+
});
473+
expect(normalizeCodeFixActions(res.body!)).toStrictEqual(
474+
normalizeCodeFixActions([
475+
{
476+
fixName: 'fixMissingCSSRule',
477+
changes: [
478+
{
479+
fileName: formatPath(iff.paths['a.module.css']),
480+
textChanges: [
481+
{
482+
start: { line: 1, offset: 1 },
483+
end: { line: 1, offset: 1 },
484+
newText: '\n.a_1 {\n \n}',
485+
},
486+
],
487+
},
488+
],
489+
},
490+
]),
491+
);
492+
});
445493
});

packages/ts-plugin/src/language-service/feature/code-fix.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import type { CMKConfig, Resolver } from '@css-modules-kit/core';
2-
import { isComponentFileName, isCSSModuleFile } from '@css-modules-kit/core';
2+
import { isCSSModuleFile } from '@css-modules-kit/core';
33
import type { Language } from '@volar/language-core';
44
import ts from 'typescript';
5-
import { isCSSModuleScript } from '../../language-plugin.js';
65
import { convertDefaultImportsToNamespaceImports, createPreferencesForCompletion } from '../../util.js';
76

87
// ref: https://github.com/microsoft/TypeScript/blob/220706eb0320ff46fad8bf80a5e99db624ee7dfb/src/compiler/diagnosticMessages.json
98
export const CANNOT_FIND_NAME_ERROR_CODE = 2304;
10-
export const PROPERTY_DOES_NOT_EXIST_ERROR_CODE = 2339;
9+
export const PROPERTY_DOES_NOT_EXIST_ERROR_CODES: [number, number] = [2339, 2551];
1110

1211
export function getCodeFixesAtPosition(
1312
language: Language<string>,
@@ -34,17 +33,15 @@ export function getCodeFixesAtPosition(
3433
excludeNamedImports(prior, fileName, resolver);
3534
}
3635

37-
if (isComponentFileName(fileName)) {
38-
// If a user is trying to use a non-existent token (e.g. `styles.nonExistToken`), provide a code fix to add the token.
39-
if (errorCodes.includes(PROPERTY_DOES_NOT_EXIST_ERROR_CODE)) {
40-
const tokenConsumer = getTokenConsumerAtPosition(fileName, start, language, languageService, project);
41-
if (tokenConsumer) {
42-
prior.push({
43-
fixName: 'fixMissingCSSRule',
44-
description: `Add missing CSS rule '.${tokenConsumer.tokenName}'`,
45-
changes: [createInsertRuleFileChange(tokenConsumer.from, tokenConsumer.tokenName, language)],
46-
});
47-
}
36+
// If a user is trying to use a non-existent token (e.g. `styles.nonExistToken`), provide a code fix to add the token.
37+
if (errorCodes.some((errorCode) => PROPERTY_DOES_NOT_EXIST_ERROR_CODES.includes(errorCode))) {
38+
const tokenConsumer = getTokenConsumerAtPosition(fileName, start, languageService, project, config);
39+
if (tokenConsumer) {
40+
prior.push({
41+
fixName: 'fixMissingCSSRule',
42+
description: `Add missing CSS rule '.${tokenConsumer.tokenName}'`,
43+
changes: [createInsertRuleFileChange(tokenConsumer.from, tokenConsumer.tokenName, language)],
44+
});
4845
}
4946
}
5047

@@ -85,9 +82,9 @@ interface TokenConsumer {
8582
function getTokenConsumerAtPosition(
8683
fileName: string,
8784
position: number,
88-
language: Language<string>,
8985
languageService: ts.LanguageService,
9086
project: ts.server.Project,
87+
config: CMKConfig,
9188
): TokenConsumer | undefined {
9289
const sourceFile = project.getSourceFile(project.projectService.toPath(fileName));
9390
if (!sourceFile) return undefined;
@@ -99,11 +96,19 @@ function getTokenConsumerAtPosition(
9996
// `expression` is the expression of the property access expression (e.g. `styles` in `styles.foo`).
10097
const expression = propertyAccessExpression.expression;
10198

102-
const definitions = languageService.getDefinitionAtPosition(fileName, expression.getStart());
103-
if (definitions && definitions[0]) {
104-
const script = language.scripts.get(definitions[0].fileName);
105-
if (isCSSModuleScript(script)) {
106-
return { tokenName: propertyAccessExpression.name.text, from: definitions[0].fileName };
99+
let [definition] = languageService.getDefinitionAtPosition(fileName, expression.getStart()) ?? [];
100+
if (!definition) return undefined;
101+
102+
// `definition` is may be `styles` definition in CSS Modules file.
103+
if (isCSSModuleFile(definition.fileName)) {
104+
return { tokenName: propertyAccessExpression.name.text, from: definition.fileName };
105+
} else if (config.namedExports) {
106+
// If namespaced import is used, it may be a definition in a component file
107+
// (e.g. the `styles` of `import * as styles from './a.module.css'`).
108+
// In that case, we need to call `getDefinitionAtPosition` again to get the definition in CSS module file.
109+
[definition] = languageService.getDefinitionAtPosition(definition.fileName, definition.textSpan.start) ?? [];
110+
if (definition && isCSSModuleFile(definition.fileName)) {
111+
return { tokenName: propertyAccessExpression.name.text, from: definition.fileName };
107112
}
108113
}
109114
return undefined;

0 commit comments

Comments
 (0)