From 25bacd180df033a09152a30be045f437fe23f9f6 Mon Sep 17 00:00:00 2001 From: Owen Rumney Date: Sat, 11 Jul 2026 10:10:30 +0100 Subject: [PATCH] fix: correctly parse exports --- .github/workflows/golangci-lint.yml | 25 +++++++ .github/workflows/pr-tests.yml | 18 +++++ .github/workflows/vscode-quality.yml | 29 ++++++++ internal/handler/handler_test.go | 15 ++++ internal/parser/parser.go | 27 ++++++-- internal/parser/parser_test.go | 14 ++++ vscode-make-ls/package.json | 3 + vscode-make-ls/scripts/package.js | 100 ++++++++++++++++++++------- vscode-make-ls/src/extension.ts | 6 +- 9 files changed, 202 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .github/workflows/pr-tests.yml create mode 100644 .github/workflows/vscode-quality.yml diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 00000000..4e040d83 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,25 @@ +name: golangci-lint + +on: + pull_request: + workflow_dispatch: + +jobs: + golangci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Install golangci-lint + run: | + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: Run golangci-lint + run: | + golangci-lint version + golangci-lint run diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml new file mode 100644 index 00000000..c8109e91 --- /dev/null +++ b/.github/workflows/pr-tests.yml @@ -0,0 +1,18 @@ +name: PR Tests + +on: + pull_request: + workflow_dispatch: + +jobs: + go-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run Go tests + run: go test ./... diff --git a/.github/workflows/vscode-quality.yml b/.github/workflows/vscode-quality.yml new file mode 100644 index 00000000..e3df62d7 --- /dev/null +++ b/.github/workflows/vscode-quality.yml @@ -0,0 +1,29 @@ +name: VS Code Extension Quality + +on: + pull_request: + workflow_dispatch: + +jobs: + vscode-quality: + runs-on: ubuntu-latest + defaults: + run: + working-directory: vscode-make-ls + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: vscode-make-ls/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Check formatting + run: npm run format:check + + - name: Check types and build + run: npm run lint diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go index 2b508da7..0fa147a6 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/handler_test.go @@ -433,6 +433,21 @@ func TestDefinitionFromUnexportVarName(t *testing.T) { assert.Equal(t, 0, locs[0].Range.Start.Line) // FOO defined on line 0 } +func TestDefinitionFromExportVarNameToOverrideVar(t *testing.T) { + harness := newHarness(t) + + input := "override KBUILD_VERBOSE :=\nexport quiet Q KBUILD_VERBOSE\n" + require.NoError(t, harness.DidOpen(testURI, "makefile", input)) + + // Cursor on "KBUILD_VERBOSE" in "export quiet Q KBUILD_VERBOSE" + locs, err := harness.Definition(testURI, 1, 15) + require.NoError(t, err) + require.Len(t, locs, 1) + assert.Equal(t, 0, locs[0].Range.Start.Line) + assert.Equal(t, 9, locs[0].Range.Start.Character) + assert.Equal(t, 23, locs[0].Range.End.Character) +} + func TestReferencesForTarget(t *testing.T) { harness := newHarness(t) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 37c8d9b7..f4726db1 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -260,7 +260,7 @@ func (p *parser) parseLine() { Flavour: model.FlavourForOp(model.VarOp(m[3])), TargetScope: targetName, Range: lineRange(startLine, 0, len(line)), - NameRange: nameRange(startLine, trimmed, m[2]), + NameRange: nameRangeInSegment(startLine, line, trimmed, m[2]), Refs: extractVarRefs(m[4], startLine), } p.variables = append(p.variables, v) @@ -333,7 +333,7 @@ func (p *parser) parseLine() { IsDoubleColon: isDouble, DocComment: p.buildDocComment(), Range: lineRange(startLine, 0, len(line)), - NameRange: nameRange(startLine, trimmed, namesPart), + NameRange: nameRangeInSegment(startLine, line, trimmed, namesPart), } p.targets = append(p.targets, t) p.currentTarget = t @@ -363,7 +363,7 @@ func (p *parser) tryParseVarAssign(s string, startLine int, fullLine string) *mo Op: op, Flavour: model.FlavourForOp(op), Range: lineRange(startLine, 0, len(fullLine)), - NameRange: nameRange(startLine, s, strings.TrimSpace(m[2])), + NameRange: nameRangeInSegment(startLine, fullLine, s, strings.TrimSpace(m[2])), Refs: extractVarRefs(m[4], startLine), } } @@ -610,13 +610,26 @@ func lineRange(line, startChar, endChar int) lsp.Range { } func nameRange(line int, fullLine, name string) lsp.Range { - idx := strings.Index(fullLine, name) + return nameRangeInSegment(line, fullLine, fullLine, name) +} + +func nameRangeInSegment(line int, fullLine, segment, name string) lsp.Range { + base := strings.Index(fullLine, segment) + if base < 0 { + base = 0 + } + idx := strings.Index(segment, name) if idx < 0 { - idx = 0 + idx = strings.Index(fullLine, name) + base = 0 + if idx < 0 { + idx = 0 + } } + col := base + idx return lsp.Range{ - Start: lsp.Position{Line: line, Character: idx}, - End: lsp.Position{Line: line, Character: idx + len(name)}, + Start: lsp.Position{Line: line, Character: col}, + End: lsp.Position{Line: line, Character: col + len(name)}, } } diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 20b23116..6096e527 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -295,6 +295,20 @@ func TestParseOverrideVar(t *testing.T) { v := m.Variables[0] assert.Equal(t, "CC", v.Name) assert.True(t, v.Override) + assert.Equal(t, 9, v.NameRange.Start.Character) + assert.Equal(t, 11, v.NameRange.End.Character) +} + +func TestParseExportVarNameRange(t *testing.T) { + input := "export\tINSTALL_PATH ?= /boot" + m := Parse(testURI, input) + + require.Len(t, m.Variables, 1) + v := m.Variables[0] + assert.Equal(t, "INSTALL_PATH", v.Name) + assert.True(t, v.Export) + assert.Equal(t, 7, v.NameRange.Start.Character) + assert.Equal(t, 19, v.NameRange.End.Character) } func TestParseVpathDirective(t *testing.T) { diff --git a/vscode-make-ls/package.json b/vscode-make-ls/package.json index e3648cb0..0db8b55d 100644 --- a/vscode-make-ls/package.json +++ b/vscode-make-ls/package.json @@ -55,6 +55,9 @@ "scripts": { "compile": "esbuild src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node --sourcemap", "watch": "npm run compile -- --watch", + "typecheck": "tsc --noEmit", + "lint": "npm run typecheck && npm run compile", + "format:check": "npx prettier@3.6.2 --check \"src/**/*.ts\" \"scripts/**/*.js\" \"*.json\"", "package": "npm run compile && node scripts/package.js", "vscode:prepublish": "esbuild src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node --minify" }, diff --git a/vscode-make-ls/scripts/package.js b/vscode-make-ls/scripts/package.js index 58d24fbd..ad68cb5f 100644 --- a/vscode-make-ls/scripts/package.js +++ b/vscode-make-ls/scripts/package.js @@ -13,21 +13,59 @@ const targets = [ { goos: "linux", goarch: "arm64", vscodeTarget: "linux-arm64" }, ]; -// Sync version from git tag (v1.2.3 → 1.2.3) so package.json never drifts. -try { - const tag = execSync("git describe --tags --exact-match", { cwd: ROOT, encoding: "utf-8" }).trim(); - const version = tag.replace(/^v/, ""); - if (/^\d+\.\d+\.\d+$/.test(version)) { - const pkgPath = path.join(EXT_DIR, "package.json"); - const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); - if (pkg.version !== version) { - console.log(`Updating package.json version: ${pkg.version} → ${version}`); - pkg.version = version; - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); +function parseSemver(tag) { + const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag); + if (!m) return null; + return { tag, parts: [Number(m[1]), Number(m[2]), Number(m[3])] }; +} + +function compareSemverDesc(a, b) { + for (let i = 0; i < 3; i++) { + if (a.parts[i] !== b.parts[i]) return b.parts[i] - a.parts[i]; + } + return 0; +} + +function resolveReleaseTag() { + const refTag = process.env.GITHUB_REF_NAME; + if (parseSemver(refTag || "")) { + return refTag; + } + + try { + const tags = execSync("git tag --points-at HEAD", { + cwd: ROOT, + encoding: "utf-8", + }) + .split("\n") + .map((s) => s.trim()) + .filter(Boolean) + .map(parseSemver) + .filter(Boolean) + .sort(compareSemverDesc); + if (tags.length > 0) { + return tags[0].tag; } + } catch { + // Fall through to package.json version. } -} catch { - console.log("No exact git tag found, using version from package.json."); + + return null; +} + +// Sync version from release tag (v1.2.3 → 1.2.3) so package.json never drifts. +const tag = resolveReleaseTag(); +if (tag) { + const version = tag.replace(/^v/, ""); + const pkgPath = path.join(EXT_DIR, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + if (pkg.version !== version) { + console.log(`Updating package.json version: ${pkg.version} → ${version}`); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + } +} else { + console.log("No release tag found, using version from package.json."); } // Copy README from repo root so vsce includes it in the .vsix. @@ -60,7 +98,7 @@ for (const t of targets) { console.log(`Compiling ${t.goos}/${t.goarch}...`); execSync( `GOOS=${t.goos} GOARCH=${t.goarch} CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o ${outPath} ./cmd/make-ls`, - { cwd: ROOT, stdio: "inherit" } + { cwd: ROOT, stdio: "inherit" }, ); // Make executable. @@ -96,17 +134,24 @@ if (process.env.VSCODE_PUBLISH_TOKEN) { const vsixPath = path.join(EXT_DIR, f); console.log(` ${f}`); try { - execSync(`npx vsce publish --pat ${process.env.VSCODE_PUBLISH_TOKEN} --packagePath ${vsixPath}`, { - cwd: EXT_DIR, - stdio: ["ignore", "inherit", "inherit"], - timeout: 120_000, - }); + execSync( + `npx vsce publish --pat ${process.env.VSCODE_PUBLISH_TOKEN} --packagePath ${vsixPath}`, + { + cwd: EXT_DIR, + stdio: ["ignore", "inherit", "inherit"], + timeout: 120_000, + }, + ); } catch (err) { - console.error(` Failed to publish ${f} to VS Code Marketplace: ${err.message}`); + console.error( + ` Failed to publish ${f} to VS Code Marketplace: ${err.message}`, + ); } } } else { - console.log("\nSkipping VS Code Marketplace publish (no VSCODE_PUBLISH_TOKEN)."); + console.log( + "\nSkipping VS Code Marketplace publish (no VSCODE_PUBLISH_TOKEN).", + ); } // Publish to Open VSX. @@ -116,11 +161,14 @@ if (process.env.OPVSX_PUBLISH_TOKEN) { const vsixPath = path.join(EXT_DIR, f); console.log(` ${f}`); try { - execSync(`npx ovsx publish ${vsixPath} -p ${process.env.OPVSX_PUBLISH_TOKEN}`, { - cwd: EXT_DIR, - stdio: ["ignore", "inherit", "inherit"], - timeout: 120_000, - }); + execSync( + `npx ovsx publish ${vsixPath} -p ${process.env.OPVSX_PUBLISH_TOKEN}`, + { + cwd: EXT_DIR, + stdio: ["ignore", "inherit", "inherit"], + timeout: 120_000, + }, + ); } catch (err) { console.error(` Failed to publish ${f} to Open VSX: ${err.message}`); } diff --git a/vscode-make-ls/src/extension.ts b/vscode-make-ls/src/extension.ts index 642dd33b..577ad61d 100644 --- a/vscode-make-ls/src/extension.ts +++ b/vscode-make-ls/src/extension.ts @@ -20,7 +20,9 @@ export function activate(context: ExtensionContext): void { const clientOptions: LanguageClientOptions = { documentSelector: [{ scheme: "file", language: "makefile" }], synchronize: { - fileEvents: workspace.createFileSystemWatcher("**/{Makefile,makefile,GNUmakefile,*.mk,*.mak}"), + fileEvents: workspace.createFileSystemWatcher( + "**/{Makefile,makefile,GNUmakefile,*.mk,*.mak}", + ), }, }; @@ -28,7 +30,7 @@ export function activate(context: ExtensionContext): void { "make-ls", "Makefile Language Server", serverOptions, - clientOptions + clientOptions, ); client.start();