From 62498b5b15af04c26b3a802253d025a1ffac7f72 Mon Sep 17 00:00:00 2001 From: Wei Wang Date: Fri, 2 Oct 2026 15:52:10 -0400 Subject: [PATCH] Find a TOML project when scanning a directory, not only when named a file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `loadDirectory` filtered on an extension set, so the one supported file that has no distinguishing extension was invisible to it: a project converted with `matlab.project.DefinitionFiles.Toml` keeps no `.prj` and no `resources/`, and its whole definition is a file called `matlab.toml`. `loadFromPath` opened that file fine from the moment the reader landed, which is the asymmetry — point a Node consumer at the file and it reads the project, point it at the folder the file is in and the folder reads as holding nothing. The filter now asks `isTomlProjectFile` beside the extension set rather than adding `.toml` to it, which would call every `Cargo.toml` and `pyproject.toml` in a folder a MATLAB project. The predicate rather than a literal, because this is the second path over one rule: `ingest` dispatches a project on `isProjectFile`, and a scan that disagrees with it either hides a file that opens fine or lists one nothing can open. Proven not to be vacuous before being trusted: with the source change stashed, the new test fails with `expected [] to deeply equal [ 'matlab.toml' ]` — the reported symptom exactly. Its marker is MATLAB-written, copied from the parity artifact, and two decoys are written beside it so their absence from the result is the filter rejecting them and not an empty directory. Affects Node consumers of this package only; the VS Code extension discovers files through its own `findFiles` glob and never calls `loadDirectory`, so nothing downstream changes and no pin needs bumping. --- dist/node/index.d.ts.map | 2 +- dist/node/index.js | 21 ++++++++++++++++++++- dist/node/index.js.map | 2 +- src/node/index.ts | 22 +++++++++++++++++++++- test/node-loader.test.ts | 27 +++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/dist/node/index.d.ts.map b/dist/node/index.d.ts.map index 0dad06d..7d7be0d 100644 --- a/dist/node/index.d.ts.map +++ b/dist/node/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAYA,OAAO,wCAAwC,CAAC;AAEhD,OAAO,EAAE,aAAa,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAwBlF,eAAO,MAAM,aAAa,uBAAiB,CAAC;AAE5C,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAOxE;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAAE,GAAG,WAAW,EAAE,CAiBpG"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAYA,OAAO,wCAAwC,CAAC;AAEhD,OAAO,EAAE,aAAa,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,qCAAqC,CAAC;AA4ClF,eAAO,MAAM,aAAa,uBAAiB,CAAC;AAE5C,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAOxE;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAAE,GAAG,WAAW,EAAE,CAiBpG"} \ No newline at end of file diff --git a/dist/node/index.js b/dist/node/index.js index f72a27a..7f5b3fd 100644 --- a/dist/node/index.js +++ b/dist/node/index.js @@ -14,6 +14,7 @@ import { setNativeInflate } from '../datamodel/parser/Inflate.js'; import { createSession as _createSession } from '../core/DataModel.js'; import { ingest } from '../core/ingest.js'; import { reasonOf } from '../datamodel/parser/ParseWarning.js'; +import { isTomlProjectFile } from '../datamodel/fileKinds.js'; // Arm the fast inflate engine explicitly. `Inflate.ts` can find `node:zlib` on its // own through `process.getBuiltinModule`, but that method only exists from Node 22.3, // and this package supports consumers on older hosts — the VS Code extension declares @@ -33,6 +34,24 @@ setNativeInflate({ zlibHead: (prefix) => inflateSync(prefix, { finishFlush: zlibConstants.Z_SYNC_FLUSH }), }); const SUPPORTED = new Set(['.sldd', '.slx', '.mdl', '.mat', '.prj']); +/** + * Whether a directory entry is something `loadFromPath` can open. + * + * An extension set AND a name test, because one supported file is identified by its + * NAME: a TOML-format project's entire definition is a file called `matlab.toml`, with + * no `.prj` marker and no `resources/` left to find it by. Matching `.toml` instead + * would call every `Cargo.toml` and `pyproject.toml` in a folder a MATLAB project — + * see `isTomlProjectFile`, which is why that predicate exists. + * + * It is that shared predicate and deliberately not a literal here, because this is the + * second path over the same rule: `ingest` dispatches a project on `isProjectFile`, so + * a scan that disagrees either hides a file that opens fine or lists one nothing can + * open. Keeping the extensions as a set is still right — they are this module's own + * business, and `ingest` sniffs CONTENT rather than extension for all of them. + */ +function isSupportedName(name) { + return SUPPORTED.has(extname(name).toLowerCase()) || isTomlProjectFile(name); +} // Re-export createSession so Node consumers can import everything from one place. export const createSession = _createSession; export function loadFromPath(session, path) { @@ -65,7 +84,7 @@ export function loadFromPath(session, path) { * minus the stderr write. Same shape as SlddNode.parse's sink. */ export function loadDirectory(session, dir, skipped) { - const names = readdirSync(dir).filter((n) => SUPPORTED.has(extname(n).toLowerCase())).sort(); + const names = readdirSync(dir).filter(isSupportedName).sort(); const out = []; for (const name of names) { try { diff --git a/dist/node/index.js.map b/dist/node/index.js.map index 96b4c40..c72980c 100644 --- a/dist/node/index.js.map +++ b/dist/node/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/src/node/index.ts b/src/node/index.ts index b0ddce8..6b8c64c 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -17,6 +17,7 @@ import { ingest } from '../core/ingest.js'; import type { Session } from '../core/DataModel.js'; import type { ISourceNode } from '../core/NodeInterfaces.js'; import { reasonOf, type ParseWarning } from '../datamodel/parser/ParseWarning.js'; +import { isTomlProjectFile } from '../datamodel/fileKinds.js'; // Arm the fast inflate engine explicitly. `Inflate.ts` can find `node:zlib` on its // own through `process.getBuiltinModule`, but that method only exists from Node 22.3, @@ -39,6 +40,25 @@ setNativeInflate({ const SUPPORTED = new Set(['.sldd', '.slx', '.mdl', '.mat', '.prj']); +/** + * Whether a directory entry is something `loadFromPath` can open. + * + * An extension set AND a name test, because one supported file is identified by its + * NAME: a TOML-format project's entire definition is a file called `matlab.toml`, with + * no `.prj` marker and no `resources/` left to find it by. Matching `.toml` instead + * would call every `Cargo.toml` and `pyproject.toml` in a folder a MATLAB project — + * see `isTomlProjectFile`, which is why that predicate exists. + * + * It is that shared predicate and deliberately not a literal here, because this is the + * second path over the same rule: `ingest` dispatches a project on `isProjectFile`, so + * a scan that disagrees either hides a file that opens fine or lists one nothing can + * open. Keeping the extensions as a set is still right — they are this module's own + * business, and `ingest` sniffs CONTENT rather than extension for all of them. + */ +function isSupportedName(name: string): boolean { + return SUPPORTED.has(extname(name).toLowerCase()) || isTomlProjectFile(name); +} + // Re-export createSession so Node consumers can import everything from one place. export const createSession = _createSession; @@ -73,7 +93,7 @@ export function loadFromPath(session: Session, path: string): ISourceNode { * minus the stderr write. Same shape as SlddNode.parse's sink. */ export function loadDirectory(session: Session, dir: string, skipped?: ParseWarning[]): ISourceNode[] { - const names = readdirSync(dir).filter((n) => SUPPORTED.has(extname(n).toLowerCase())).sort(); + const names = readdirSync(dir).filter(isSupportedName).sort(); const out: ISourceNode[] = []; for (const name of names) { try { diff --git a/test/node-loader.test.ts b/test/node-loader.test.ts index b48e3b0..5e99f8a 100644 --- a/test/node-loader.test.ts +++ b/test/node-loader.test.ts @@ -147,4 +147,31 @@ describe('node loader', () => { expect(loaded.map((n) => n.name)).toEqual(['legacy.MDL', 'upper.SLDD']); expect(errors).toEqual([]); }); + + it('loadDirectory finds a TOML project by NAME, and no other .toml', () => { + // The one supported file an extension set cannot express. A project converted with + // `matlab.project.DefinitionFiles.Toml` has no `.prj` and no `resources/` — its whole + // definition is a file called `matlab.toml` — so a scan filtering on extensions alone + // reads a real project directory as holding nothing, which is how this started. + // + // The marker is MATLAB-WRITTEN, copied from the parity artifact rather than typed + // here: a hand-authored TOML standing in for what MATLAB writes is how a reader comes + // to depend on a spelling MATLAB never uses. + const dir = mkdtempSync(join(tmpdir(), 'dex-toml-dir-')); + copyFileSync( + fileURLToPath(new URL('./parity/artifacts/project/Toml/parityProject/matlab.toml', import.meta.url)), + join(dir, 'matlab.toml'), + ); + // The hazard the NAME test exists for, written so its absence below is the filter + // rejecting it and not an empty directory: `.toml` is the config format of half the + // tooling a MATLAB repository sits beside. + writeFileSync(join(dir, 'Cargo.toml'), '[package]\nname = "not-a-matlab-project"\n'); + writeFileSync(join(dir, 'pyproject.toml'), '[project]\nname = "also-not"\n'); + + const skipped: ParseWarning[] = []; + const loaded = loadDirectory(createSession(), dir, skipped); + + expect(loaded.map((n) => n.name)).toEqual(['matlab.toml']); + expect(skipped).toEqual([]); // listed AND opened — a listing that then fails is worse + }); });