Skip to content

Commit f5716f0

Browse files
authored
fix: handle version ranges correctly (#49)
* feat: handle version ranges correctly * chore: update * refactor: use better name * apply suggestions from coderabbit * fix: catch each rule's error separately * fix: don't compare version when it's a tag * test: fix * fix: resolve version in hover provider * extract `resolveExactVersion` * prefer `Object.hasOwn` * feat: check url package starts with `github` * compare upgrade with exactVersion * refactor(diagnostics): extract version resolution into DiagnosticContext * refactor: extract isSupportedProtocol * test: add all diagnostics tests * test: update * feat(upgrade): add diagnostics target * test: update * update * update * update
1 parent 3778859 commit f5716f0

25 files changed

Lines changed: 949 additions & 174 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
"jest-mock-vscode": "catalog:test",
165165
"jsonc-parser": "catalog:inline",
166166
"module-replacements": "catalog:inline",
167+
"msw": "catalog:test",
167168
"nano-staged": "catalog:dev",
168169
"ofetch": "catalog:inline",
169170
"perfect-debounce": "catalog:inline",

playground/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"nuxt": "npm:4.3.0"
66
},
77
"devDependencies": {
8+
"ofetch": "^2.0.0-alpha.1",
89
"array-includes": "latest",
910
"axios": "",
1011
"is-number": "",

pnpm-lock.yaml

Lines changed: 388 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ catalogs:
2323
yaml: ^2.8.2
2424
test:
2525
jest-mock-vscode: ^4.11.0
26+
msw: ^2.12.10
2627
vite-tsconfig-paths: ^6.1.1
2728
vitest: ^4.0.18

src/providers/completion-item/version.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { CompletionItemProvider, Position, TextDocument } from 'vscode'
33
import { PRERELEASE_PATTERN } from '#constants'
44
import { config } from '#state'
55
import { getPackageInfo } from '#utils/api/package'
6-
import { formatVersion, isSupportedProtocol, parseVersion } from '#utils/version'
6+
import { formatUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version'
77
import { CompletionItem, CompletionItemKind } from 'vscode'
88

99
export class VersionCompletionItemProvider<T extends Extractor> implements CompletionItemProvider {
@@ -39,25 +39,25 @@ export class VersionCompletionItemProvider<T extends Extractor> implements Compl
3939

4040
const items: CompletionItem[] = []
4141

42-
for (const semver in pkg.versionsMeta) {
43-
const meta = pkg.versionsMeta[semver]
42+
for (const version in pkg.versionsMeta) {
43+
const meta = pkg.versionsMeta[version]
4444

4545
if (meta.deprecated != null)
4646
continue
4747

48-
if (config.completion.excludePrerelease && PRERELEASE_PATTERN.test(semver))
48+
if (config.completion.excludePrerelease && PRERELEASE_PATTERN.test(version))
4949
continue
5050

5151
if (config.completion.version === 'provenance-only' && !meta.provenance)
5252
continue
5353

54-
const text = formatVersion({ ...parsed, semver })
54+
const text = formatUpgradeVersion(parsed, version)
5555
const item = new CompletionItem(text, CompletionItemKind.Value)
5656

5757
item.range = this.extractor.getNodeRange(document, versionNode)
5858
item.insertText = text
5959

60-
const tag = pkg.versionToTag.get(semver)
60+
const tag = pkg.versionToTag.get(version)
6161
if (tag)
6262
item.detail = tag
6363

src/providers/diagnostics/index.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { DependencyInfo, ValidNode } from '#types/extractor'
22
import type { PackageInfo } from '#utils/api/package'
3+
import type { ParsedVersion } from '#utils/version'
34
import type { Awaitable } from 'reactive-vscode'
45
import type { Diagnostic, TextDocument } from 'vscode'
56
import { useActiveExtractor } from '#composables/active-extractor'
67
import { config, logger } from '#state'
78
import { getPackageInfo } from '#utils/api/package'
9+
import { resolveExactVersion } from '#utils/package'
10+
import { isSupportedProtocol, parseVersion } from '#utils/version'
811
import { debounce } from 'perfect-debounce'
912
import { computed, useActiveTextEditor, useDisposable, useDocumentText, watch } from 'reactive-vscode'
1013
import { languages } from 'vscode'
@@ -15,10 +18,17 @@ import { checkReplacement } from './rules/replacement'
1518
import { checkUpgrade } from './rules/upgrade'
1619
import { checkVulnerability } from './rules/vulnerability'
1720

21+
export interface DiagnosticContext {
22+
dep: DependencyInfo
23+
pkg: PackageInfo
24+
parsed: ParsedVersion | null
25+
exactVersion: string | null
26+
}
27+
1828
export interface NodeDiagnosticInfo extends Omit<Diagnostic, 'range' | 'source'> {
1929
node: ValidNode
2030
}
21-
export type DiagnosticRule = (dep: DependencyInfo, pkg: PackageInfo) => Awaitable<NodeDiagnosticInfo | undefined>
31+
export type DiagnosticRule = (ctx: DiagnosticContext) => Awaitable<NodeDiagnosticInfo | undefined>
2232

2333
export function useDiagnostics() {
2434
const diagnosticCollection = useDisposable(languages.createDiagnosticCollection(displayName))
@@ -86,20 +96,28 @@ export function useDiagnostics() {
8696
if (!pkg)
8797
continue
8898

99+
const parsed = parseVersion(dep.version)
100+
const exactVersion = parsed && isSupportedProtocol(parsed.protocol)
101+
? resolveExactVersion(pkg, parsed.version)
102+
: null
103+
89104
for (const rule of rules) {
90-
const diagnostic = await rule(dep, pkg)
91-
if (isDocumentChanged(document, targetUri, targetVersion))
92-
return
93-
if (!diagnostic)
94-
continue
95-
96-
diagnostics.push({
97-
source: displayName,
98-
range: extractor.getNodeRange(document, diagnostic.node),
99-
...diagnostic,
100-
})
101-
102-
flush(document, targetUri, targetVersion, diagnostics)
105+
try {
106+
const diagnostic = await rule({ dep, pkg, parsed, exactVersion })
107+
if (isDocumentChanged(document, targetUri, targetVersion))
108+
return
109+
if (!diagnostic)
110+
continue
111+
112+
diagnostics.push({
113+
source: displayName,
114+
range: extractor.getNodeRange(document, diagnostic.node),
115+
...diagnostic,
116+
})
117+
flush(document, targetUri, targetVersion, diagnostics)
118+
} catch (err) {
119+
logger.warn(`Fail to check ${dep.name} (${rule.name}): ${err}`)
120+
}
103121
}
104122
} catch (err) {
105123
logger.warn(`Failed to check ${dep.name}: ${err}`)

src/providers/diagnostics/rules/deprecation.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
import type { DiagnosticRule } from '..'
22
import { npmxPackageUrl } from '#utils/links'
3-
import { isSupportedProtocol, parseVersion } from '#utils/version'
43
import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode'
54

6-
export const checkDeprecation: DiagnosticRule = (dep, pkg) => {
7-
const parsed = parseVersion(dep.version)
8-
if (!parsed || !isSupportedProtocol(parsed.protocol))
5+
export const checkDeprecation: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => {
6+
if (!parsed || !exactVersion)
97
return
108

11-
const { semver } = parsed
12-
const versionInfo = pkg.versionsMeta[semver]
9+
const versionInfo = pkg.versionsMeta[exactVersion]
1310

14-
if (!versionInfo?.deprecated)
11+
if (!versionInfo.deprecated)
1512
return
1613

1714
return {
1815
node: dep.versionNode,
19-
message: `${dep.name} v${semver} has been deprecated: ${versionInfo.deprecated}`,
16+
message: `${dep.name} v${exactVersion} has been deprecated: ${versionInfo.deprecated}`,
2017
severity: DiagnosticSeverity.Error,
2118
code: {
2219
value: 'deprecation',
23-
target: Uri.parse(npmxPackageUrl(dep.name, semver)),
20+
target: Uri.parse(npmxPackageUrl(dep.name, parsed.version)),
2421
},
2522
tags: [DiagnosticTag.Deprecated],
2623
}

src/providers/diagnostics/rules/dist-tag.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import type { DiagnosticRule } from '..'
22
import { npmxPackageUrl } from '#utils/links'
3-
import { isSupportedProtocol, parseVersion } from '#utils/version'
43
import { DiagnosticSeverity, Uri } from 'vscode'
54

6-
export const checkDistTag: DiagnosticRule = (dep, pkg) => {
7-
const parsed = parseVersion(dep.version)
8-
if (!parsed || !isSupportedProtocol(parsed.protocol))
5+
export const checkDistTag: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => {
6+
if (!parsed || !exactVersion)
97
return
108

11-
const tag = parsed.semver
12-
if (!(tag in pkg.distTags))
9+
const tag = parsed.version
10+
if (!Object.hasOwn(pkg.distTags, tag))
1311
return
1412

1513
return {

src/providers/diagnostics/rules/replacement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function getReplacementInfo(replacement: ModuleReplacement) {
3939
}
4040
}
4141

42-
export const checkReplacement: DiagnosticRule = async (dep) => {
42+
export const checkReplacement: DiagnosticRule = async ({ dep }) => {
4343
const replacement = await getReplacement(dep.name)
4444
if (!replacement)
4545
return

src/providers/diagnostics/rules/upgrade.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,37 @@
11
import type { DependencyInfo } from '#types/extractor'
22
import type { ParsedVersion } from '#utils/version'
33
import type { DiagnosticRule, NodeDiagnosticInfo } from '..'
4-
import { formatVersion, isSupportedProtocol, parseVersion } from '#utils/version'
4+
import { npmxPackageUrl } from '#utils/links'
5+
import { formatUpgradeVersion } from '#utils/version'
6+
import gt from 'semver/functions/gt'
7+
import lte from 'semver/functions/lte'
58
import prerelease from 'semver/functions/prerelease'
6-
import gtr from 'semver/ranges/gtr'
7-
import ltr from 'semver/ranges/ltr'
8-
import { DiagnosticSeverity } from 'vscode'
9+
import { DiagnosticSeverity, Uri } from 'vscode'
910

10-
function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, upgradeVersion: string): NodeDiagnosticInfo {
11-
const target = formatVersion({ ...parsed, semver: upgradeVersion })
11+
function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, target: string): NodeDiagnosticInfo {
1212
return {
1313
node: dep.versionNode,
1414
severity: DiagnosticSeverity.Hint,
15-
message: `New version available: ${target}`,
16-
code: 'upgrade',
15+
message: `New version available: ${formatUpgradeVersion(parsed, target)}`,
16+
code: {
17+
value: 'upgrade',
18+
target: Uri.parse(npmxPackageUrl(dep.name, target)),
19+
},
1720
}
1821
}
1922

20-
export const checkUpgrade: DiagnosticRule = (dep, pkg) => {
21-
const parsed = parseVersion(dep.version)
22-
if (!parsed || !isSupportedProtocol(parsed.protocol))
23+
export const checkUpgrade: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => {
24+
if (!parsed || !exactVersion)
2325
return
2426

25-
const { semver } = parsed
26-
const latest = pkg.distTags.latest
27+
if (Object.hasOwn(pkg.distTags, exactVersion))
28+
return
2729

28-
if (latest && gtr(latest, semver))
30+
const { latest } = pkg.distTags
31+
if (gt(latest, exactVersion))
2932
return createUpgradeDiagnostic(dep, parsed, latest)
3033

31-
const currentPreId = prerelease(semver)?.[0]
34+
const currentPreId = prerelease(exactVersion)?.[0]
3235
if (currentPreId == null)
3336
return
3437

@@ -37,7 +40,7 @@ export const checkUpgrade: DiagnosticRule = (dep, pkg) => {
3740
continue
3841
if (prerelease(tagVersion)?.[0] !== currentPreId)
3942
continue
40-
if (ltr(tagVersion, semver))
43+
if (lte(tagVersion, exactVersion))
4144
continue
4245

4346
return createUpgradeDiagnostic(dep, parsed, tagVersion)

0 commit comments

Comments
 (0)