Skip to content

Commit

Permalink
Generalize glob implementation (#1537)
Browse files Browse the repository at this point in the history
Adds an abstraction between glob implementation and the filesystem so it can be applied against arbitrary sources of
hierarchical data.

Also moves doc generation to a different theme that seems better maintained.
  • Loading branch information
lauckhart authored Dec 19, 2024
1 parent bdb08fb commit ad0dea6
Show file tree
Hide file tree
Showing 16 changed files with 1,266 additions and 1,305 deletions.
2 changes: 1 addition & 1 deletion chip-testing/src/cluster/TestWindowCoveringServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type MoveData = {
* waiting 1s.
*/
export class TestWindowCoveringServer extends TestWindowCoveringServerBase {
protected declare internal: TestWindowCoveringServer.Internal;
declare protected internal: TestWindowCoveringServer.Internal;

override initialize() {
logger.info("TestWindowCoveringServer initialized");
Expand Down
2,277 changes: 1,092 additions & 1,185 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const MAX_CURRENT_LEVEL = 0xfe;
* data ranges.
*/
export class ColorControlServerLogic extends ColorControlServerBase {
protected declare internal: ColorControlServerLogic.Internal;
declare protected internal: ColorControlServerLogic.Internal;
declare state: ColorControlServerLogic.State;

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const schema = Base.schema!.extend({
* Endpoint gets initialized.
*/
export class GeneralDiagnosticsServer extends Base {
protected declare internal: GeneralDiagnosticsServer.Internal;
declare protected internal: GeneralDiagnosticsServer.Internal;
declare state: GeneralDiagnosticsServer.State;
schema = schema;

Expand Down
2 changes: 1 addition & 1 deletion packages/node/src/behaviors/identify/IdentifyServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { IdentifyBehavior } from "./IdentifyBehavior.js";
* * `stopIdentifying` - Emitted when the device stops identifying.
*/
export class IdentifyServer extends IdentifyBehavior {
protected declare internal: IdentifyServer.Internal;
declare protected internal: IdentifyServer.Internal;
declare state: IdentifyServer.State;
declare events: IdentifyServer.Events;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const LevelControlLogicBase = LevelControlBehavior.with(LevelControl.Feature.OnO
* All overridable methods except setRemainingTime can be implemented sync or async by returning a Promise.
*/
export class LevelControlServerLogic extends LevelControlLogicBase {
protected declare internal: LevelControlServerLogic.Internal;
declare protected internal: LevelControlServerLogic.Internal;
declare state: LevelControlServerLogic.State;

/** Returns the minimum level, including feature specific fallback value handling. */
Expand Down
2 changes: 1 addition & 1 deletion packages/node/src/behaviors/on-off/OnOffServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const Base = OnOffBehavior.with(OnOff.Feature.Lighting);
* specific, so this needs to be implemented by the device implementor as needed.
*/
export class OnOffServer extends Base {
protected declare internal: OnOffServer.Internal;
declare protected internal: OnOffServer.Internal;

override initialize() {
if (this.features.lighting && this.#getBootReason() !== GeneralDiagnostics.BootReason.SoftwareUpdateCompleted) {
Expand Down
2 changes: 1 addition & 1 deletion packages/node/src/behaviors/switch/SwitchServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const schema = SwitchServerBase.schema!.extend({
* Switch (MS) feature is used.
*/
export class SwitchServerLogic extends SwitchServerBase {
protected declare internal: SwitchServerLogic.Internal;
declare protected internal: SwitchServerLogic.Internal;
declare state: SwitchServerLogic.State;
declare events: SwitchServerLogic.Events;
schema = schema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const WC_PERCENT100THS_COEFFICIENT = 100;
* implementation.
*/
export class WindowCoveringServerLogic extends WindowCoveringServerBase {
protected declare internal: WindowCoveringServerLogic.Internal;
declare protected internal: WindowCoveringServerLogic.Internal;
declare state: WindowCoveringServerLogic.State;

override initialize() {
Expand Down
2 changes: 1 addition & 1 deletion packages/tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"typescript": "~5.7.2",
"type-fest": "^4.30.1",
"typedoc": "^0.27.4",
"typedoc-material-theme": "^1.2.0",
"typedoc-github-theme": "^0.2.0",
"@microsoft/tsdoc": "^0.15.1"
},
"optionalDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/tools/src/building/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Graph } from "./graph.js";
// NOTE - this is a "best attempt" at doc generation via typescript. The result is not all that great; typedoc is too
// limited for our complex API so we're going to need something custom to do it right

const PLUGINS = ["typedoc-material-theme"] as string[];
const PLUGINS = ["typedoc-github-theme"] as string[];

// Double "docs" directories so top-level directory can be a GH repository with pages configured under "docs"
const OUTPUT_PATH = "build/docs/docs";
Expand Down
1 change: 1 addition & 0 deletions packages/tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export * from "./building/project.js";
export * from "./running/ensure-compiled.js";
export * from "./util/commander.js";
export * from "./util/file.js";
export * from "./util/glob.js";
export * from "./util/package.js";
export * from "./util/progress.js";
150 changes: 41 additions & 109 deletions packages/tools/src/util/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import { readdirSync, readFileSync, statSync } from "fs";
import { GLOBSTAR, Minimatch, ParseReturnFiltered } from "minimatch";
import { resolve } from "path";
import { ignoreErrorSync } from "./errors.js";

Expand Down Expand Up @@ -49,120 +48,53 @@ export function maybeReaddirSync(path: string) {
}
}

export class GlobError extends Error {}

export function globSync(pattern: string | string[]) {
if (typeof pattern === "string") {
return [...globOneSync(pattern)];
}

const result = Array<string>();
for (const p of pattern) {
result.push(...globOneSync(p));
}

return result;
export function isDirectory(path: string) {
return !!ignoreErrorSync("ENOENT", () => statSync(path).isDirectory());
}

function globOneSync(pattern: string) {
// Parse the glob
const mm = new Minimatch(pattern.replace(/\\/g, "/"), {});
const results = new Set<string>();
for (const part of mm.set) {
for (const path of globOnePartSync(mm, part)) {
results.add(path);
}
}
return results;
export function isFile(path: string) {
return !!ignoreErrorSync("ENOENT", () => statSync(path).isFile());
}

function globOnePartSync(mm: Minimatch, segments: ParseReturnFiltered[]) {
// Find the starting path
let rootPath = "";
let didOne = false;
while (typeof segments[0] === "string") {
if (didOne) {
rootPath += "/";
} else {
didOne = true;
}
rootPath += segments.shift() as string;
}

// If we are out of segments, this is not a glob. Just check for presence
if (!segments.length) {
const stat = maybeStatSync(rootPath);

if (stat?.[rootPath.endsWith("/") ? "isDirectory" : "isFile"]()) {
return [rootPath];
}

return [];
}

// Walk filesystem and apply glob
const results = new Set<string>();

function match(path: string, segments: ParseReturnFiltered[]) {
// If the filter is empty then match current path
if (!segments.length) {
results.add(path);
return;
}

// If filter starts without magic then just stat that one path
if (typeof segments[0] === "string") {
const subpath = resolve(path, segments[0]);
if (maybeStatSync(resolve(path, segments[0]))) {
match(subpath, segments.slice(1));
return;
}
}

// If filter is just GLOBSTAR then all paths match but search continues
if (segments.length === 1 && segments[0] === GLOBSTAR) {
results.add(path);
}

// Filter starts with magic so load directory entries to match
const subnames = maybeReaddirSync(path);
if (!subnames) {
return;
}

// Test each directory entry
for (const subname of subnames) {
const subpath = resolve(path, subname);

// Anything but GLOBSTAR is 1:1 subname/segment match
if (segments[0] !== GLOBSTAR) {
if (mm.matchOne([subname], segments, true)) {
match(subpath, segments.slice(1));
}
continue;
}

// GLOBSTAR matches nothing so test second segment
if (segments.length > 1) {
if (mm.matchOne([subname], segments.slice(1), true)) {
match(subpath, segments.slice(2));
}
}

// GLOBSTAR matches everything
match(subpath, segments);
}
}

match(rootPath, segments);

return results;
/**
* Tiny virtual filesystem driver. Currently used only for processing globs.
*/
export interface FilesystemSync<T extends FilesystemSync.Stat = FilesystemSync.Stat> {
resolve(...segments: string[]): string;
readdir(path: string): string[] | undefined;
stat(path: string): T | undefined;
}

export function isDirectory(path: string) {
return !!ignoreErrorSync("ENOENT", () => statSync(path).isDirectory());
export function FilesystemSync(): FilesystemSync<FilesystemSync.Stat> {
return {
resolve(...segments) {
return resolve(...segments);
},

readdir(path) {
return maybeReaddirSync(path);
},

stat(path) {
const stats = maybeStatSync(path);
if (stats) {
return {
get isFile() {
return stats.isFile();
},

get isDirectory() {
return stats.isDirectory();
},
};
}
},
};
}

export function isFile(path: string) {
return !!ignoreErrorSync("ENOENT", () => statSync(path).isFile());
export namespace FilesystemSync {
export interface Stat {
isDirectory?: boolean;
isFile?: boolean;
}
}
118 changes: 118 additions & 0 deletions packages/tools/src/util/glob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* @license
* Copyright 2022-2024 Matter.js Authors
* SPDX-License-Identifier: Apache-2.0
*/

import { GLOBSTAR, Minimatch, ParseReturnFiltered } from "minimatch";
import { FilesystemSync } from "./file.js";

export class GlobError extends Error {}

export function globSync(pattern: string | string[], fs = FilesystemSync()) {
if (typeof pattern === "string") {
return [...globOneSync(pattern, fs)];
}

const result = Array<string>();
for (const p of pattern) {
result.push(...globOneSync(p, fs));
}

return result;
}

function globOneSync(pattern: string, fs: FilesystemSync) {
// Parse the glob
const mm = new Minimatch(pattern.replace(/\\/g, "/"), {});
const results = new Set<string>();
for (const part of mm.set) {
for (const path of globOnePartSync(mm, part, fs)) {
results.add(path);
}
}
return results;
}

function globOnePartSync(mm: Minimatch, segments: ParseReturnFiltered[], fs: FilesystemSync) {
// Find the starting path
let rootPath = "";
let didOne = false;
while (typeof segments[0] === "string") {
if (didOne) {
rootPath += "/";
} else {
didOne = true;
}
rootPath += segments.shift() as string;
}

// If we are out of segments, this is not a glob. Just check for presence
if (!segments.length) {
const stat = fs.stat(rootPath);

if (stat?.[rootPath.endsWith("/") ? "isDirectory" : "isFile"]) {
return [rootPath];
}

return [];
}

// Walk filesystem and apply glob
const results = new Set<string>();

function match(path: string, segments: ParseReturnFiltered[]) {
// If the filter is empty then match current path
if (!segments.length) {
results.add(path);
return;
}

// If filter starts without magic then just stat that one path
if (typeof segments[0] === "string") {
const subpath = fs.resolve(path, segments[0]);
if (fs.stat(fs.resolve(path, segments[0]))) {
match(subpath, segments.slice(1));
return;
}
}

// If filter is just GLOBSTAR then all paths match but search continues
if (segments.length === 1 && segments[0] === GLOBSTAR) {
results.add(path);
}

// Filter starts with magic so load directory entries to match
const subnames = fs.readdir(path);
if (!subnames) {
return;
}

// Test each directory entry
for (const subname of subnames) {
const subpath = fs.resolve(path, subname);

// Anything but GLOBSTAR is 1:1 subname/segment match
if (segments[0] !== GLOBSTAR) {
if (mm.matchOne([subname], segments, true)) {
match(subpath, segments.slice(1));
}
continue;
}

// GLOBSTAR matches nothing so test second segment
if (segments.length > 1) {
if (mm.matchOne([subname], segments.slice(1), true)) {
match(subpath, segments.slice(2));
}
}

// GLOBSTAR matches everything
match(subpath, segments);
}
}

match(rootPath, segments);

return results;
}
1 change: 1 addition & 0 deletions packages/tools/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
export * from "./commander.js";
export * from "./errors.js";
export * from "./file.js";
export * from "./glob.js";
export * from "./package.js";
Loading

0 comments on commit ad0dea6

Please sign in to comment.