Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const path = require("path");
const fs = require("fs");
const os = require("os");
const cp = require("child_process");
const { findFiles } = require("./lib/utils");
const TmpDir = os.tmpdir();

class PHPCBF {
Expand Down Expand Up @@ -116,7 +117,7 @@ class PHPCBF {
];

const fileDir = path.relative(workspaceRoot, path.dirname(filePath));
const confFile = this.findFiles(workspaceRoot, fileDir, confFileNames);
const confFile = findFiles(workspaceRoot, fileDir, confFileNames);

standard = confFile || this.standard;
} else {
Expand All @@ -126,27 +127,6 @@ class PHPCBF {
return standard;
}

findFiles(parent, directory, name) {
const names = [].concat(name);
const chunks = path.resolve(parent, directory).split(path.sep);

while (chunks.length) {
let currentDir = chunks.join(path.sep);
for (const fileName of names) {
const filePath = path.join(currentDir, fileName);
if (fs.existsSync(filePath)) {
return filePath;
}
}
if (parent === currentDir) {
break;
}
chunks.pop();
}

return null;
}

format(document) {
// Reload settings scoped to this document so multi-root workspaces and
// per-folder settings are respected on every format call.
Expand Down
35 changes: 35 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use strict";
const path = require("path");
const fs = require("fs");

/**
* Walk up the directory tree from `parent/directory` looking for any file
* whose name matches one of the entries in `name`. Stops at `parent`.
*
* @param {string} parent - Root of the workspace (walk stops here).
* @param {string} directory - Relative directory to start from.
* @param {string|string[]} name - File name(s) to look for.
* @returns {string|null} Absolute path to the first matching file, or null.
*/
function findFiles(parent, directory, name) {
const names = [].concat(name);
const chunks = path.resolve(parent, directory).split(path.sep);

while (chunks.length) {
let currentDir = chunks.join(path.sep);
for (const fileName of names) {
const filePath = path.join(currentDir, fileName);
if (fs.existsSync(filePath)) {
return filePath;
}
}
if (parent === currentDir) {
break;
}
chunks.pop();
}

return null;
}

module.exports = { findFiles };
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
},
"scripts": {
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test"
"test": "node ./node_modules/vscode/bin/test",
"test:unit": "node --test test/unit.test.js"
},
"devDependencies": {
"typescript": "^2.6.1",
Expand Down
31 changes: 25 additions & 6 deletions test/extension.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,33 @@ const assert = require('assert');
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
const vscode = require('vscode');
const myExtension = require('../extension');

// Defines a Mocha test suite to group tests of similar kind together
suite("Extension Tests", function() {

// Defines a Mocha unit test
test("Something 1", function() {
assert.equal(-1, [1, 2, 3].indexOf(5));
assert.equal(-1, [1, 2, 3].indexOf(0));
test("Extension is present", function() {
assert.ok(vscode.extensions.getExtension("persoderlind.phpcbf"));
});
});

test("Extension activates for PHP documents", function(done) {
const ext = vscode.extensions.getExtension("persoderlind.phpcbf");
if (!ext) {
// Extension not installed in test host — skip gracefully
done();
return;
}
ext.activate().then(
() => { done(); },
(err) => { done(err); }
);
});

test("phpcbf-soderlind command is registered", function() {
return vscode.commands.getCommands(true).then(commands => {
assert.ok(
commands.includes("phpcbf-soderlind"),
"Expected phpcbf-soderlind command to be registered"
);
});
});
});
106 changes: 106 additions & 0 deletions test/unit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"use strict";
/**
* Unit tests for pure utility functions in lib/utils.js.
* Run with: npm run test:unit
* Requires Node.js 18+ (uses built-in test runner).
*/
const { test, describe, before, after } = require("node:test");
const assert = require("node:assert/strict");
const path = require("path");
const fs = require("fs");
const os = require("os");

const { findFiles } = require("../lib/utils");

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

let tmpRoot;

function mkDir(...parts) {
const dir = path.join(tmpRoot, ...parts);
fs.mkdirSync(dir, { recursive: true });
return dir;
}

function mkFile(...parts) {
const filePath = path.join(tmpRoot, ...parts);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, "");
return filePath;
}

before(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "phpcbf-unit-"));
});

after(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
});

// ---------------------------------------------------------------------------
// findFiles
// ---------------------------------------------------------------------------

describe("findFiles", () => {
test("returns path when file exists in exact directory", () => {
const subDir = mkDir("exact", "sub");
const expected = mkFile("exact", "sub", "phpcs.xml");
const result = findFiles(path.join(tmpRoot, "exact"), "sub", "phpcs.xml");
assert.equal(result, expected);
});

test("returns null when file does not exist anywhere in tree", () => {
mkDir("missing", "a", "b");
const result = findFiles(path.join(tmpRoot, "missing"), path.join("a", "b"), "phpcs.xml");
assert.equal(result, null);
});

test("walks up the directory tree to find file", () => {
// Place the config at the root (parent), start searching from deep subdir
const expected = mkFile("walkup", "phpcs.xml");
mkDir("walkup", "a", "b", "c");
const result = findFiles(path.join(tmpRoot, "walkup"), path.join("a", "b", "c"), "phpcs.xml");
assert.equal(result, expected);
});

test("returns file in the deepest matching directory when multiple exist", () => {
// File exists at root AND a sub-directory — deepest should win because
// we search from deepest upward and return the first match.
mkFile("multi", "phpcs.xml");
const deeper = mkFile("multi", "src", "phpcs.xml");
const result = findFiles(path.join(tmpRoot, "multi"), "src", "phpcs.xml");
assert.equal(result, deeper);
});

test("accepts an array of file names and returns first match found", () => {
mkDir("array", "sub");
const expected = mkFile("array", "sub", ".phpcs.xml");
const result = findFiles(
path.join(tmpRoot, "array"),
"sub",
[".phpcs.xml", "phpcs.xml", "phpcs.xml.dist"]
);
assert.equal(result, expected);
});

test("stops at parent boundary — does not escape above parent", () => {
// The parent is "boundary/inner". A file exists one level above (in tmpRoot)
// but findFiles should NOT reach it.
mkFile("boundary-file.xml");
mkDir("boundary", "inner");
const result = findFiles(
path.join(tmpRoot, "boundary", "inner"),
".",
"boundary-file.xml"
);
assert.equal(result, null);
});

test("handles a single-segment directory (file at root)", () => {
const expected = mkFile("single", "phpcs.xml");
const result = findFiles(path.join(tmpRoot, "single"), ".", "phpcs.xml");
assert.equal(result, expected);
});
});