From d4e49919c4ca0724e1b2939a10422d371a397c8d Mon Sep 17 00:00:00 2001 From: Egor Aristov Date: Sat, 25 Oct 2025 17:31:24 +0300 Subject: [PATCH] tons of code --- Makefile | 51 +- kitcom/internal/tsgo/ast/ast.go | 20 + kitcom/internal/tsgo/ast/symbol.go | 1 - kitcom/internal/tsgo/ast/utilities.go | 2 +- kitcom/internal/tsgo/astnav/tokens.go | 653 ------------------ kitcom/internal/tsgo/core/core.go | 27 +- .../tsgo/diagnostics/diagnostics_generated.go | 2 +- kitcom/internal/tsgo/scanner/scanner.go | 3 + kitcom/internal/tsgo/stringutil/compare.go | 20 + kitcom/internal/tsgo/tspath/extension.go | 2 +- .../internal/tsgo/vfs/cachedvfs/cachedvfs.go | 150 ---- kitcom/internal/tsgo/vfs/internal/internal.go | 189 ----- kitcom/internal/tsgo/vfs/iovfs/iofs.go | 207 ------ kitcom/internal/tsgo/vfs/osvfs/os.go | 177 ----- .../internal/tsgo/vfs/osvfs/realpath_other.go | 11 - .../tsgo/vfs/osvfs/realpath_windows.go | 100 --- kitcom/internal/tsgo/vfs/utilities.go | 464 ------------- kitcom/internal/tsgo/vfs/vfs.go | 79 --- .../tsgo/vfs/vfsmock/mock_generated.go | 536 -------------- kitcom/internal/tsgo/vfs/vfsmock/wrapper.go | 20 - kitcom/internal/tsgo/vfs/vfstest/vfstest.go | 614 ---------------- 21 files changed, 111 insertions(+), 3217 deletions(-) delete mode 100644 kitcom/internal/tsgo/astnav/tokens.go delete mode 100644 kitcom/internal/tsgo/vfs/cachedvfs/cachedvfs.go delete mode 100644 kitcom/internal/tsgo/vfs/internal/internal.go delete mode 100644 kitcom/internal/tsgo/vfs/iovfs/iofs.go delete mode 100644 kitcom/internal/tsgo/vfs/osvfs/os.go delete mode 100644 kitcom/internal/tsgo/vfs/osvfs/realpath_other.go delete mode 100644 kitcom/internal/tsgo/vfs/osvfs/realpath_windows.go delete mode 100644 kitcom/internal/tsgo/vfs/utilities.go delete mode 100644 kitcom/internal/tsgo/vfs/vfs.go delete mode 100644 kitcom/internal/tsgo/vfs/vfsmock/mock_generated.go delete mode 100644 kitcom/internal/tsgo/vfs/vfsmock/wrapper.go delete mode 100644 kitcom/internal/tsgo/vfs/vfstest/vfstest.go diff --git a/Makefile b/Makefile index 836f690..13b1fc1 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,51 @@ + +SHELL := /bin/bash +tsgo_dir = ./kitcom/internal/tsgo +my_package = efprojects.com/kitten-ipc/kitcom/internal/tsgo + default: @echo "Please read Makefile for available targets" vendor_tsgo: - @mkdir -p ./kitcom/internal/tsgo + @mkdir -p $(tsgo_dir) @git clone --depth 1 https://github.com/microsoft/typescript-go - @echo Renaming packages... - @find ./typescript-go/internal -type file -name "*.go" -exec sed -i -e 's!"github.com/microsoft/typescript-go/internal!"efprojects.com/kitten-ipc/kitcom/internal/tsgo!g' {} \; - @cp -r ./typescript-go/internal/* ./kitcom/internal/tsgo - @git add ./kitcom/internal/ - @echo Cleaning up... + @find ./typescript-go/internal -type file -name "*.go" -exec sed -i -e 's!"github.com/microsoft/typescript-go/internal!"$(my_package)!g' {} \; + @cp -r ./typescript-go/internal/* $(tsgo_dir) @rm -rf @rm -rf typescript-go - echo Successfully copied tsgo code and renamed packages. remove_tsgo_tests: - @find ./kitcom/internal/tsgo -name "*_test.go" -exec rm {} \; + @find $(tsgo_dir) -name "*_test.go" -exec rm {} \; -.PHONY: vendor_tsgo remove_tsgo_tests +# just for "fun" +remove_tsgo_unused: + @set -e ; \ + dirs=`find $(tsgo_dir) -type d -mindepth 1 -maxdepth 1` ; \ + nessesary_old="parser " ; \ + nessesary="$$nessesary_old" ; \ + while true; do \ + for d in $$dirs; do \ + pkg=`basename "$$d"` ; \ + for usedIn in $$nessesary; do \ + if grep -q -R "$(my_package)/$$pkg" "$(tsgo_dir)/$$usedIn" > /dev/null; then \ + if [[ "$$nessesary" != *"$$pkg "* ]]; then \ + nessesary="$$nessesary $$pkg " ; \ + fi ; \ + break ; \ + fi ; \ + done ; \ + done ; \ + if [[ "$$nessesary" == "$$nessesary_old" ]]; then \ + break ; \ + fi ; \ + nessesary_old="$$nessesary" ; \ + done ; \ + for d in $$dirs; do \ + pkg=`basename $$d` ; \ + if [[ "$$nessesary" != *"$$pkg "* ]]; then \ + echo "removing $$pkg" ; \ + rm -rf $(tsgo_dir)/$$pkg ; \ + fi ; \ + done + + +.PHONY: vendor_tsgo remove_tsgo_tests remove_tsgo_unused diff --git a/kitcom/internal/tsgo/ast/ast.go b/kitcom/internal/tsgo/ast/ast.go index 992a742..853d6d1 100644 --- a/kitcom/internal/tsgo/ast/ast.go +++ b/kitcom/internal/tsgo/ast/ast.go @@ -8706,6 +8706,10 @@ func (node *TemplateHead) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewTemplateHead(node.Text, node.RawText, node.TemplateFlags), node.AsNode(), f.AsNodeFactory().hooks) } +func IsTemplateHead(node *Node) bool { + return node.Kind == KindTemplateHead +} + // TemplateMiddle type TemplateMiddle struct { @@ -8726,6 +8730,10 @@ func (node *TemplateMiddle) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewTemplateMiddle(node.Text, node.RawText, node.TemplateFlags), node.AsNode(), f.AsNodeFactory().hooks) } +func IsTemplateMiddle(node *Node) bool { + return node.Kind == KindTemplateMiddle +} + // TemplateTail type TemplateTail struct { @@ -8746,6 +8754,10 @@ func (node *TemplateTail) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewTemplateTail(node.Text, node.RawText, node.TemplateFlags), node.AsNode(), f.AsNodeFactory().hooks) } +func IsTemplateTail(node *Node) bool { + return node.Kind == KindTemplateTail +} + // TemplateLiteralTypeNode type TemplateLiteralTypeNode struct { @@ -9635,6 +9647,10 @@ func (node *JSDocTypeExpression) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewJSDocTypeExpression(node.Type), node.AsNode(), f.AsNodeFactory().hooks) } +func IsJSDocTypeExpression(node *Node) bool { + return node.Kind == KindJSDocTypeExpression +} + // JSDocNonNullableType type JSDocNonNullableType struct { @@ -10565,6 +10581,10 @@ func (node *JSDocTypeLiteral) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewJSDocTypeLiteral(node.JSDocPropertyTags, node.IsArrayType), node.AsNode(), f.AsNodeFactory().hooks) } +func IsJSDocTypeLiteral(node *Node) bool { + return node.Kind == KindJSDocTypeLiteral +} + // JSDocSignature type JSDocSignature struct { TypeNodeBase diff --git a/kitcom/internal/tsgo/ast/symbol.go b/kitcom/internal/tsgo/ast/symbol.go index 691c628..02ae1be 100644 --- a/kitcom/internal/tsgo/ast/symbol.go +++ b/kitcom/internal/tsgo/ast/symbol.go @@ -43,7 +43,6 @@ const ( InternalSymbolNameClass = InternalSymbolNamePrefix + "class" // Unnamed class expression InternalSymbolNameFunction = InternalSymbolNamePrefix + "function" // Unnamed function expression InternalSymbolNameComputed = InternalSymbolNamePrefix + "computed" // Computed property name declaration with dynamic name - InternalSymbolNameResolving = InternalSymbolNamePrefix + "resolving" // Indicator symbol used to mark partially resolved type aliases InternalSymbolNameInstantiationExpression = InternalSymbolNamePrefix + "instantiationExpression" // Instantiation expressions InternalSymbolNameImportAttributes = InternalSymbolNamePrefix + "importAttributes" InternalSymbolNameExportEquals = "export=" // Export assignment symbol diff --git a/kitcom/internal/tsgo/ast/utilities.go b/kitcom/internal/tsgo/ast/utilities.go index 94cc458..f569280 100644 --- a/kitcom/internal/tsgo/ast/utilities.go +++ b/kitcom/internal/tsgo/ast/utilities.go @@ -2641,7 +2641,7 @@ func GetNodeAtPosition(file *SourceFile, position int, includeJSDoc bool) *Node } if child == nil { current.ForEachChild(func(node *Node) bool { - if nodeContainsPosition(node, position) { + if nodeContainsPosition(node, position) && node.Kind != KindJSExportAssignment && node.Kind != KindCommonJSExport { child = node return true } diff --git a/kitcom/internal/tsgo/astnav/tokens.go b/kitcom/internal/tsgo/astnav/tokens.go deleted file mode 100644 index 6be7fd3..0000000 --- a/kitcom/internal/tsgo/astnav/tokens.go +++ /dev/null @@ -1,653 +0,0 @@ -package astnav - -import ( - "fmt" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" -) - -func GetTouchingPropertyName(sourceFile *ast.SourceFile, position int) *ast.Node { - return getReparsedNodeForNode(getTokenAtPosition(sourceFile, position, false /*allowPositionInLeadingTrivia*/, func(node *ast.Node) bool { - return ast.IsPropertyNameLiteral(node) || ast.IsKeywordKind(node.Kind) || ast.IsPrivateIdentifier(node) - })) -} - -// If the given node is a declaration name node in a JSDoc comment that is subject to reparsing, return the declaration name node -// for the corresponding reparsed construct. Otherwise, just return the node. -func getReparsedNodeForNode(node *ast.Node) *ast.Node { - if node.Flags&ast.NodeFlagsJSDoc != 0 && (ast.IsIdentifier(node) || ast.IsPrivateIdentifier(node)) { - parent := node.Parent - if (ast.IsJSDocTypedefTag(parent) || ast.IsJSDocCallbackTag(parent) || ast.IsJSDocPropertyTag(parent) || ast.IsJSDocParameterTag(parent) || ast.IsImportClause(parent) || ast.IsImportSpecifier(parent)) && parent.Name() == node { - // Reparsing preserves the location of the name. Thus, a search at the position of the name with JSDoc excluded - // finds the containing reparsed declaration node. - if reparsed := ast.GetNodeAtPosition(ast.GetSourceFileOfNode(node), node.Pos(), false); reparsed != nil { - if name := reparsed.Name(); name != nil && name.Pos() == node.Pos() { - return name - } - } - } - } - return node -} - -func GetTouchingToken(sourceFile *ast.SourceFile, position int) *ast.Node { - return getTokenAtPosition(sourceFile, position, false /*allowPositionInLeadingTrivia*/, nil) -} - -func GetTokenAtPosition(sourceFile *ast.SourceFile, position int) *ast.Node { - return getTokenAtPosition(sourceFile, position, true /*allowPositionInLeadingTrivia*/, nil) -} - -func getTokenAtPosition( - sourceFile *ast.SourceFile, - position int, - allowPositionInLeadingTrivia bool, - includePrecedingTokenAtEndPosition func(node *ast.Node) bool, -) *ast.Node { - // getTokenAtPosition returns a token at the given position in the source file. - // The token can be a real node in the AST, or a synthesized token constructed - // with information from the scanner. Synthesized tokens are only created when - // needed, and they are stored in the source file's token cache such that multiple - // calls to getTokenAtPosition with the same position will return the same object - // in memory. If there is no token at the given position (possible when - // `allowPositionInLeadingTrivia` is false), the lowest node that encloses the - // position is returned. - - // `next` tracks the node whose children will be visited on the next iteration. - // `prevSubtree` is a node whose end position is equal to the target position, - // only if `includePrecedingTokenAtEndPosition` is provided. Once set, the next - // iteration of the loop will test the rightmost token of `prevSubtree` to see - // if it should be returned. - var next, prevSubtree *ast.Node - current := sourceFile.AsNode() - // `left` tracks the lower boundary of the node/token that could be returned, - // and is eventually the scanner's start position, if the scanner is used. - left := 0 - - testNode := func(node *ast.Node) int { - if node.Kind != ast.KindEndOfFile && node.End() == position && includePrecedingTokenAtEndPosition != nil { - prevSubtree = node - } - - if node.End() < position || node.Kind != ast.KindEndOfFile && node.End() == position { - return -1 - } - if getPosition(node, sourceFile, allowPositionInLeadingTrivia) > position { - return 1 - } - return 0 - } - - // We zero in on the node that contains the target position by visiting each - // child and JSDoc comment of the current node. Node children are walked in - // order, while node lists are binary searched. - visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { - // We can't abort visiting children, so once a match is found, we set `next` - // and do nothing on subsequent visits. - if node != nil && node.Flags&ast.NodeFlagsReparsed == 0 && next == nil { - switch testNode(node) { - case -1: - if !ast.IsJSDocKind(node.Kind) { - // We can't move the left boundary into or beyond JSDoc, - // because we may end up returning the token after this JSDoc, - // constructing it with the scanner, and we need to include - // all its leading trivia in its position. - left = node.End() - } - case 0: - next = node - } - } - return node - } - - visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { - if nodeList != nil && len(nodeList.Nodes) > 0 && next == nil { - if nodeList.End() == position && includePrecedingTokenAtEndPosition != nil { - left = nodeList.End() - prevSubtree = nodeList.Nodes[len(nodeList.Nodes)-1] - } else if nodeList.End() <= position { - left = nodeList.End() - } else if nodeList.Pos() <= position { - nodes := nodeList.Nodes - index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int { - if node.Flags&ast.NodeFlagsReparsed != 0 { - return 0 - } - cmp := testNode(node) - if cmp < 0 { - left = node.End() - } - return cmp - }) - if match && nodes[index].Flags&ast.NodeFlagsReparsed != 0 { - // filter and search again - nodes = core.Filter(nodes, func(node *ast.Node) bool { - return node.Flags&ast.NodeFlagsReparsed == 0 - }) - index, match = core.BinarySearchUniqueFunc(nodes, func(middle int, node *ast.Node) int { - cmp := testNode(node) - if cmp < 0 { - left = node.End() - } - return cmp - }) - } - if match { - next = nodes[index] - } - } - } - return nodeList - } - - for { - VisitEachChildAndJSDoc(current, sourceFile, visitNode, visitNodeList) - // If prevSubtree was set on the last iteration, it ends at the target position. - // Check if the rightmost token of prevSubtree should be returned based on the - // `includePrecedingTokenAtEndPosition` callback. - if prevSubtree != nil { - child := FindPrecedingTokenEx(sourceFile, position, prevSubtree, false /*excludeJSDoc*/) - if child != nil && child.End() == position && includePrecedingTokenAtEndPosition(child) { - // Optimization: includePrecedingTokenAtEndPosition only ever returns true - // for real AST nodes, so we don't run the scanner here. - return child - } - prevSubtree = nil - } - - // No node was found that contains the target position, so we've gone as deep as - // we can in the AST. We've either found a token, or we need to run the scanner - // to construct one that isn't stored in the AST. - if next == nil { - if ast.IsTokenKind(current.Kind) || shouldSkipChild(current) { - return current - } - scanner := scanner.GetScannerForSourceFile(sourceFile, left) - for left < current.End() { - token := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenStart := core.IfElse(allowPositionInLeadingTrivia, tokenFullStart, scanner.TokenStart()) - tokenEnd := scanner.TokenEnd() - if tokenStart <= position && (position < tokenEnd) { - if token == ast.KindIdentifier || !ast.IsTokenKind(token) { - if ast.IsJSDocKind(current.Kind) { - return current - } - panic(fmt.Sprintf("did not expect %s to have %s in its trivia", current.Kind.String(), token.String())) - } - return sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current) - } - if includePrecedingTokenAtEndPosition != nil && tokenEnd == position { - prevToken := sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, current) - if includePrecedingTokenAtEndPosition(prevToken) { - return prevToken - } - } - left = tokenEnd - scanner.Scan() - } - return current - } - current = next - left = current.Pos() - next = nil - } -} - -func getPosition(node *ast.Node, sourceFile *ast.SourceFile, allowPositionInLeadingTrivia bool) int { - if allowPositionInLeadingTrivia { - return node.Pos() - } - return scanner.GetTokenPosOfNode(node, sourceFile, true /*includeJSDoc*/) -} - -func findRightmostNode(node *ast.Node) *ast.Node { - var next *ast.Node - current := node - visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { - if node != nil { - next = node - } - return node - } - visitNodes := func(nodeList *ast.NodeList, visitor *ast.NodeVisitor) *ast.NodeList { - if nodeList != nil { - if rightmost := ast.FindLastVisibleNode(nodeList.Nodes); rightmost != nil { - next = rightmost - } - } - return nodeList - } - visitor := getNodeVisitor(visitNode, visitNodes) - - for { - current.VisitEachChild(visitor) - if next == nil { - return current - } - current = next - next = nil - } -} - -func VisitEachChildAndJSDoc( - node *ast.Node, - sourceFile *ast.SourceFile, - visitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node, - visitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList, -) { - visitor := getNodeVisitor(visitNode, visitNodes) - if node.Flags&ast.NodeFlagsHasJSDoc != 0 { - for _, jsdoc := range node.JSDoc(sourceFile) { - if visitor.Hooks.VisitNode != nil { - visitor.Hooks.VisitNode(jsdoc, visitor) - } else { - visitor.VisitNode(jsdoc) - } - } - } - node.VisitEachChild(visitor) -} - -const ( - comparisonLessThan = -1 - comparisonEqualTo = 0 - comparisonGreaterThan = 1 -) - -// Finds the leftmost token satisfying `position < token.End()`. -// If the leftmost token satisfying `position < token.End()` is invalid, or if position -// is in the trivia of that leftmost token, -// we will find the rightmost valid token with `token.End() <= position`. -func FindPrecedingToken(sourceFile *ast.SourceFile, position int) *ast.Node { - return FindPrecedingTokenEx(sourceFile, position, nil, false) -} - -func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *ast.Node, excludeJSDoc bool) *ast.Node { - var find func(node *ast.Node) *ast.Node - find = func(n *ast.Node) *ast.Node { - if ast.IsNonWhitespaceToken(n) && n.Kind != ast.KindEndOfFile { - return n - } - - // `foundChild` is the leftmost node that contains the target position. - // `prevChild` is the last visited child of the current node. - var foundChild, prevChild *ast.Node - visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { - // skip synthesized nodes (that will exist now because of jsdoc handling) - if node == nil || node.Flags&ast.NodeFlagsReparsed != 0 { - return node - } - if foundChild != nil { // We cannot abort visiting children, so once the desired child is found, we do nothing. - return node - } - if position < node.End() && (prevChild == nil || prevChild.End() <= position) { - foundChild = node - } else { - prevChild = node - } - return node - } - visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { - if foundChild != nil { - return nodeList - } - if nodeList != nil && len(nodeList.Nodes) > 0 { - nodes := nodeList.Nodes - index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, _ *ast.Node) int { - // synthetic jsdoc nodes should have jsdocNode.End() <= n.Pos() - if nodes[middle].Flags&ast.NodeFlagsReparsed != 0 { - return comparisonLessThan - } - if position < nodes[middle].End() { - if middle == 0 || position >= nodes[middle-1].End() { - return comparisonEqualTo - } - return comparisonGreaterThan - } - return comparisonLessThan - }) - - if match { - foundChild = nodes[index] - } - - validLookupIndex := core.IfElse(match, index-1, len(nodes)-1) - for i := validLookupIndex; i >= 0; i-- { - if nodes[i].Flags&ast.NodeFlagsReparsed != 0 { - continue - } - if prevChild == nil { - prevChild = nodes[i] - } - } - } - return nodeList - } - VisitEachChildAndJSDoc(n, sourceFile, visitNode, visitNodes) - - if foundChild != nil { - // Note that the span of a node's tokens is [getStartOfNode(node, ...), node.end). - // Given that `position < child.end` and child has constituent tokens, we distinguish these cases: - // 1) `position` precedes `child`'s tokens or `child` has no tokens (ie: in a comment or whitespace preceding `child`): - // we need to find the last token in a previous child node or child tokens. - // 2) `position` is within the same span: we recurse on `child`. - start := GetStartOfNode(foundChild, sourceFile, !excludeJSDoc /*includeJSDoc*/) - lookInPreviousChild := start >= position || // cursor in the leading trivia or preceding tokens - !isValidPrecedingNode(foundChild, sourceFile) - if lookInPreviousChild { - if position >= foundChild.Pos() { - // Find jsdoc preceding the foundChild. - var jsDoc *ast.Node - nodeJSDoc := n.JSDoc(sourceFile) - for i := len(nodeJSDoc) - 1; i >= 0; i-- { - if nodeJSDoc[i].Pos() >= foundChild.Pos() { - jsDoc = nodeJSDoc[i] - break - } - } - if jsDoc != nil { - if !excludeJSDoc { - return find(jsDoc) - } else { - return findRightmostValidToken(jsDoc.End(), sourceFile, n, position, excludeJSDoc) - } - } - return findRightmostValidToken(foundChild.Pos(), sourceFile, n, -1 /*position*/, excludeJSDoc) - } else { // Answer is in tokens between two visited children. - return findRightmostValidToken(foundChild.Pos(), sourceFile, n, position, excludeJSDoc) - } - } else { - // position is in [foundChild.getStart(), foundChild.End): recur. - return find(foundChild) - } - } - - // We have two cases here: either the position is at the end of the file, - // or the desired token is in the unvisited trailing tokens of the current node. - if position >= n.End() { - return findRightmostValidToken(n.End(), sourceFile, n, -1 /*position*/, excludeJSDoc) - } else { - return findRightmostValidToken(n.End(), sourceFile, n, position, excludeJSDoc) - } - } - - var node *ast.Node - if startNode != nil { - node = startNode - } else { - node = sourceFile.AsNode() - } - result := find(node) - if result != nil && ast.IsWhitespaceOnlyJsxText(result) { - panic("Expected result to be a non-whitespace token.") - } - return result -} - -func isValidPrecedingNode(node *ast.Node, sourceFile *ast.SourceFile) bool { - start := GetStartOfNode(node, sourceFile, false /*includeJSDoc*/) - width := node.End() - start - return !(ast.IsWhitespaceOnlyJsxText(node) || width == 0) -} - -func GetStartOfNode(node *ast.Node, file *ast.SourceFile, includeJSDoc bool) int { - return scanner.GetTokenPosOfNode(node, file, includeJSDoc) -} - -// Looks for rightmost valid token in the range [startPos, endPos). -// If position is >= 0, looks for rightmost valid token that precedes or touches that position. -func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingNode *ast.Node, position int, excludeJSDoc bool) *ast.Node { - if position == -1 { - position = containingNode.End() - } - var find func(n *ast.Node, endPos int) *ast.Node - find = func(n *ast.Node, endPos int) *ast.Node { - if n == nil { - return nil - } - if ast.IsNonWhitespaceToken(n) { - return n - } - - var rightmostValidNode *ast.Node - rightmostVisitedNodes := make([]*ast.Node, 0, 1) // Nodes after the last valid node. - hasChildren := false - shouldVisitNode := func(node *ast.Node) bool { - // Node is synthetic or out of the desired range: don't visit it. - return !(node.Flags&ast.NodeFlagsReparsed != 0 || - node.End() > endPos || GetStartOfNode(node, sourceFile, !excludeJSDoc /*includeJSDoc*/) >= position) - } - visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { - if node == nil { - return node - } - hasChildren = true - if !shouldVisitNode(node) { - return node - } - rightmostVisitedNodes = append(rightmostVisitedNodes, node) - if isValidPrecedingNode(node, sourceFile) { - rightmostValidNode = node - rightmostVisitedNodes = rightmostVisitedNodes[:0] - } - return node - } - visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { - if nodeList != nil && len(nodeList.Nodes) > 0 { - hasChildren = true - index, _ := core.BinarySearchUniqueFunc(nodeList.Nodes, func(middle int, node *ast.Node) int { - if node.End() > endPos { - return comparisonGreaterThan - } - return comparisonLessThan - }) - validIndex := -1 - for i := index - 1; i >= 0; i-- { - if !shouldVisitNode(nodeList.Nodes[i]) { - continue - } - if isValidPrecedingNode(nodeList.Nodes[i], sourceFile) { - validIndex = i - rightmostValidNode = nodeList.Nodes[i] - break - } - } - for i := validIndex + 1; i < index; i++ { - if !shouldVisitNode(nodeList.Nodes[i]) { - continue - } - rightmostVisitedNodes = append(rightmostVisitedNodes, nodeList.Nodes[i]) - } - } - return nodeList - } - VisitEachChildAndJSDoc(n, sourceFile, visitNode, visitNodes) - - // Three cases: - // 1. The answer is a token of `rightmostValidNode`. - // 2. The answer is one of the unvisited tokens that occur after the rightmost valid node. - // 3. The current node is a childless, token-less node. The answer is the current node. - - // Case 2: Look at unvisited trailing tokens that occur in between the rightmost visited nodes. - if !shouldSkipChild(n) { // JSDoc nodes don't include trivia tokens as children. - var startPos int - if rightmostValidNode != nil { - startPos = rightmostValidNode.End() - } else { - startPos = n.Pos() - } - scanner := scanner.GetScannerForSourceFile(sourceFile, startPos) - var tokens []*ast.Node - for _, visitedNode := range rightmostVisitedNodes { - // Trailing tokens that occur before this node. - for startPos < min(visitedNode.Pos(), position) { - tokenStart := scanner.TokenStart() - if tokenStart >= position { - break - } - token := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - startPos = tokenEnd - tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, n)) - scanner.Scan() - } - startPos = visitedNode.End() - scanner.ResetPos(startPos) - scanner.Scan() - } - // Trailing tokens after last visited node. - for startPos < min(endPos, position) { - tokenStart := scanner.TokenStart() - if tokenStart >= position { - break - } - token := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - startPos = tokenEnd - tokens = append(tokens, sourceFile.GetOrCreateToken(token, tokenFullStart, tokenEnd, n)) - scanner.Scan() - } - - lastToken := len(tokens) - 1 - // Find preceding valid token. - for i := lastToken; i >= 0; i-- { - if !ast.IsWhitespaceOnlyJsxText(tokens[i]) { - return tokens[i] - } - } - } - - // Case 3: childless node. - if !hasChildren { - if n != containingNode { - return n - } - return nil - } - // Case 1: recur on rightmostValidNode. - if rightmostValidNode != nil { - endPos = rightmostValidNode.End() - } - return find(rightmostValidNode, endPos) - } - - return find(containingNode, endPos) -} - -func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFile) *ast.Node { - var find func(n *ast.Node) *ast.Node - find = func(n *ast.Node) *ast.Node { - if ast.IsTokenKind(n.Kind) && n.Pos() == previousToken.End() { - // this is token that starts at the end of previous token - return it - return n - } - // Node that contains `previousToken` or occurs immediately after it. - var foundNode *ast.Node - visitNode := func(node *ast.Node, _ *ast.NodeVisitor) *ast.Node { - if node != nil && node.Flags&ast.NodeFlagsReparsed == 0 && - node.Pos() <= previousToken.End() && node.End() > previousToken.End() { - foundNode = node - } - return node - } - visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { - if nodeList != nil && len(nodeList.Nodes) > 0 && foundNode == nil { - nodes := nodeList.Nodes - index, match := core.BinarySearchUniqueFunc(nodes, func(_ int, node *ast.Node) int { - if node.Flags&ast.NodeFlagsReparsed != 0 { - return comparisonLessThan - } - if node.Pos() > previousToken.End() { - return comparisonGreaterThan - } - if node.End() <= previousToken.Pos() { - return comparisonLessThan - } - return comparisonEqualTo - }) - if match { - foundNode = nodes[index] - } - } - return nodeList - } - VisitEachChildAndJSDoc(n, file, visitNode, visitNodes) - // Cases: - // 1. no answer exists - // 2. answer is an unvisited token - // 3. answer is in the visited found node - - // Case 3: look for the next token inside the found node. - if foundNode != nil { - return find(foundNode) - } - startPos := previousToken.End() - // Case 2: look for the next token directly. - if startPos >= n.Pos() && startPos < n.End() { - scanner := scanner.GetScannerForSourceFile(file, startPos) - token := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenStart := scanner.TokenStart() - tokenEnd := scanner.TokenEnd() - if tokenStart == previousToken.End() { - return file.GetOrCreateToken(token, tokenFullStart, tokenEnd, n) - } - panic(fmt.Sprintf("Expected to find next token at %d, got token %s at %d", previousToken.End(), token, tokenStart)) - } - // Case 3: no answer. - return nil - } - return find(parent) -} - -func getNodeVisitor( - visitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node, - visitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList, -) *ast.NodeVisitor { - var wrappedVisitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node - var wrappedVisitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList - if visitNode != nil { - wrappedVisitNode = func(n *ast.Node, v *ast.NodeVisitor) *ast.Node { - if ast.IsJSDocSingleCommentNodeComment(n) { - return n - } - return visitNode(n, v) - } - } - - if visitNodes != nil { - wrappedVisitNodes = func(n *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList { - if ast.IsJSDocSingleCommentNodeList(n) { - return n - } - return visitNodes(n, v) - } - } - - return ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: wrappedVisitNode, - VisitToken: wrappedVisitNode, - VisitNodes: wrappedVisitNodes, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - wrappedVisitNodes(&modifiers.NodeList, visitor) - } - return modifiers - }, - }) -} - -func shouldSkipChild(node *ast.Node) bool { - return node.Kind == ast.KindJSDoc || - node.Kind == ast.KindJSDocText || - node.Kind == ast.KindJSDocTypeLiteral || - node.Kind == ast.KindJSDocSignature || - ast.IsJSDocLinkLike(node) || - ast.IsJSDocTag(node) -} diff --git a/kitcom/internal/tsgo/core/core.go b/kitcom/internal/tsgo/core/core.go index d296308..e4c15ca 100644 --- a/kitcom/internal/tsgo/core/core.go +++ b/kitcom/internal/tsgo/core/core.go @@ -7,6 +7,7 @@ import ( "slices" "sort" "strings" + "sync" "unicode" "unicode/utf8" @@ -476,6 +477,8 @@ func GetSpellingSuggestion[T any](name string, candidates []T, getName func(T) s maximumLengthDifference := max(2, int(float64(len(name))*0.34)) bestDistance := math.Floor(float64(len(name))*0.4) + 1 // If the best result is worse than this, don't bother. runeName := []rune(name) + buffers := levenshteinBuffersPool.Get().(*levenshteinBuffers) + defer levenshteinBuffersPool.Put(buffers) var bestCandidate T for _, candidate := range candidates { candidateName := getName(candidate) @@ -490,7 +493,7 @@ func GetSpellingSuggestion[T any](name string, candidates []T, getName func(T) s if len(candidateName) < 3 && !strings.EqualFold(candidateName, name) { continue } - distance := levenshteinWithMax(runeName, []rune(candidateName), bestDistance-0.1) + distance := levenshteinWithMax(buffers, runeName, []rune(candidateName), bestDistance-0.1) if distance < 0 { continue } @@ -502,9 +505,25 @@ func GetSpellingSuggestion[T any](name string, candidates []T, getName func(T) s return bestCandidate } -func levenshteinWithMax(s1 []rune, s2 []rune, maxValue float64) float64 { - previous := make([]float64, len(s2)+1) - current := make([]float64, len(s2)+1) +type levenshteinBuffers struct { + previous []float64 + current []float64 +} + +var levenshteinBuffersPool = sync.Pool{ + New: func() any { + return &levenshteinBuffers{} + }, +} + +func levenshteinWithMax(buffers *levenshteinBuffers, s1 []rune, s2 []rune, maxValue float64) float64 { + bufferSize := len(s2) + 1 + buffers.previous = slices.Grow(buffers.previous[:0], bufferSize)[:bufferSize] + buffers.current = slices.Grow(buffers.current[:0], bufferSize)[:bufferSize] + + previous := buffers.previous + current := buffers.current + big := maxValue + 0.01 for i := range previous { previous[i] = float64(i) diff --git a/kitcom/internal/tsgo/diagnostics/diagnostics_generated.go b/kitcom/internal/tsgo/diagnostics/diagnostics_generated.go index 5c32430..c40232c 100644 --- a/kitcom/internal/tsgo/diagnostics/diagnostics_generated.go +++ b/kitcom/internal/tsgo/diagnostics/diagnostics_generated.go @@ -2326,7 +2326,7 @@ var Compiler_option_0_may_only_be_used_with_build = &Message{code: 5093, categor var Compiler_option_0_may_not_be_used_with_build = &Message{code: 5094, category: CategoryError, key: "Compiler_option_0_may_not_be_used_with_build_5094", text: "Compiler option '--{0}' may not be used with '--build'."} -var Option_0_can_only_be_used_when_module_is_set_to_preserve_or_to_es2015_or_later = &Message{code: 5095, category: CategoryError, key: "Option_0_can_only_be_used_when_module_is_set_to_preserve_or_to_es2015_or_later_5095", text: "Option '{0}' can only be used when 'module' is set to 'preserve' or to 'es2015' or later."} +var Option_0_can_only_be_used_when_module_is_set_to_preserve_commonjs_or_es2015_or_later = &Message{code: 5095, category: CategoryError, key: "Option_0_can_only_be_used_when_module_is_set_to_preserve_commonjs_or_es2015_or_later_5095", text: "Option '{0}' can only be used when 'module' is set to 'preserve', 'commonjs', or 'es2015' or later."} var Option_allowImportingTsExtensions_can_only_be_used_when_either_noEmit_or_emitDeclarationOnly_is_set = &Message{code: 5096, category: CategoryError, key: "Option_allowImportingTsExtensions_can_only_be_used_when_either_noEmit_or_emitDeclarationOnly_is_set_5096", text: "Option 'allowImportingTsExtensions' can only be used when either 'noEmit' or 'emitDeclarationOnly' is set."} diff --git a/kitcom/internal/tsgo/scanner/scanner.go b/kitcom/internal/tsgo/scanner/scanner.go index d564b10..4ba98cb 100644 --- a/kitcom/internal/tsgo/scanner/scanner.go +++ b/kitcom/internal/tsgo/scanner/scanner.go @@ -1455,6 +1455,9 @@ func (s *Scanner) scanIdentifierParts() string { func (s *Scanner) scanString(jsxAttributeString bool) string { quote := s.char() + if quote == '\'' { + s.tokenFlags |= ast.TokenFlagsSingleQuote + } s.pos++ // Fast path for simple strings without escape sequences. strLen := strings.IndexRune(s.text[s.pos:], quote) diff --git a/kitcom/internal/tsgo/stringutil/compare.go b/kitcom/internal/tsgo/stringutil/compare.go index 832e1db..0fb1cd3 100644 --- a/kitcom/internal/tsgo/stringutil/compare.go +++ b/kitcom/internal/tsgo/stringutil/compare.go @@ -98,3 +98,23 @@ func CompareStringsCaseInsensitiveThenSensitive(a, b string) Comparison { } return CompareStringsCaseSensitive(a, b) } + +// CompareStringsCaseInsensitiveEslintCompatible performs a case-insensitive comparison +// using toLowerCase() instead of toUpperCase() for ESLint compatibility. +// +// `CompareStringsCaseInsensitive` transforms letters to uppercase for unicode reasons, +// while eslint's `sort-imports` rule transforms letters to lowercase. Which one you choose +// affects the relative order of letters and ASCII characters 91-96, of which `_` is a +// valid character in an identifier. So if we used `CompareStringsCaseInsensitive` for +// import sorting, TypeScript and eslint would disagree about the correct case-insensitive +// sort order for `__String` and `Foo`. Since eslint's whole job is to create consistency +// by enforcing nitpicky details like this, it makes way more sense for us to just adopt +// their convention so users can have auto-imports without making eslint angry. +func CompareStringsCaseInsensitiveEslintCompatible(a, b string) Comparison { + if a == b { + return ComparisonEqual + } + a = strings.ToLower(a) + b = strings.ToLower(b) + return strings.Compare(a, b) +} diff --git a/kitcom/internal/tsgo/tspath/extension.go b/kitcom/internal/tsgo/tspath/extension.go index cc30d48..1b4422e 100644 --- a/kitcom/internal/tsgo/tspath/extension.go +++ b/kitcom/internal/tsgo/tspath/extension.go @@ -51,7 +51,7 @@ func RemoveFileExtension(path string) string { } } // Otherwise just remove single dot extension, if any - return path[:len(path)-len(filepath.Ext(path))] + return path[:len(path)-len(filepath.Ext(path))] //nolint:forbidigo } func TryGetExtensionFromPath(p string) string { diff --git a/kitcom/internal/tsgo/vfs/cachedvfs/cachedvfs.go b/kitcom/internal/tsgo/vfs/cachedvfs/cachedvfs.go deleted file mode 100644 index b09c0eb..0000000 --- a/kitcom/internal/tsgo/vfs/cachedvfs/cachedvfs.go +++ /dev/null @@ -1,150 +0,0 @@ -package cachedvfs - -import ( - "sync/atomic" - "time" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" -) - -type FS struct { - fs vfs.FS - enabled atomic.Bool - - directoryExistsCache collections.SyncMap[string, bool] - fileExistsCache collections.SyncMap[string, bool] - getAccessibleEntriesCache collections.SyncMap[string, vfs.Entries] - realpathCache collections.SyncMap[string, string] - statCache collections.SyncMap[string, vfs.FileInfo] -} - -var _ vfs.FS = (*FS)(nil) - -func From(fs vfs.FS) *FS { - fsys := &FS{fs: fs} - fsys.enabled.Store(true) - return fsys -} - -func (fsys *FS) DisableAndClearCache() { - if fsys.enabled.CompareAndSwap(true, false) { - fsys.ClearCache() - } -} - -func (fsys *FS) Enable() { - fsys.enabled.Store(true) -} - -func (fsys *FS) ClearCache() { - fsys.directoryExistsCache.Clear() - fsys.fileExistsCache.Clear() - fsys.getAccessibleEntriesCache.Clear() - fsys.realpathCache.Clear() - fsys.statCache.Clear() -} - -func (fsys *FS) DirectoryExists(path string) bool { - if fsys.enabled.Load() { - if ret, ok := fsys.directoryExistsCache.Load(path); ok { - return ret - } - } - - ret := fsys.fs.DirectoryExists(path) - - if fsys.enabled.Load() { - fsys.directoryExistsCache.Store(path, ret) - } - - return ret -} - -func (fsys *FS) FileExists(path string) bool { - if fsys.enabled.Load() { - if ret, ok := fsys.fileExistsCache.Load(path); ok { - return ret - } - } - - ret := fsys.fs.FileExists(path) - - if fsys.enabled.Load() { - fsys.fileExistsCache.Store(path, ret) - } - - return ret -} - -func (fsys *FS) GetAccessibleEntries(path string) vfs.Entries { - if fsys.enabled.Load() { - if ret, ok := fsys.getAccessibleEntriesCache.Load(path); ok { - return ret - } - } - - ret := fsys.fs.GetAccessibleEntries(path) - - if fsys.enabled.Load() { - fsys.getAccessibleEntriesCache.Store(path, ret) - } - - return ret -} - -func (fsys *FS) ReadFile(path string) (contents string, ok bool) { - return fsys.fs.ReadFile(path) -} - -func (fsys *FS) Realpath(path string) string { - if fsys.enabled.Load() { - if ret, ok := fsys.realpathCache.Load(path); ok { - return ret - } - } - - ret := fsys.fs.Realpath(path) - - if fsys.enabled.Load() { - fsys.realpathCache.Store(path, ret) - } - - return ret -} - -func (fsys *FS) Remove(path string) error { - return fsys.fs.Remove(path) -} - -func (fsys *FS) Chtimes(path string, aTime time.Time, mTime time.Time) error { - return fsys.fs.Chtimes(path, aTime, mTime) -} - -func (fsys *FS) Stat(path string) vfs.FileInfo { - if fsys.enabled.Load() { - if ret, ok := fsys.statCache.Load(path); ok { - return ret - } - } - - ret := fsys.fs.Stat(path) - - if fsys.enabled.Load() { - fsys.statCache.Store(path, ret) - } - - return ret -} - -func (fsys *FS) UseCaseSensitiveFileNames() bool { - return fsys.fs.UseCaseSensitiveFileNames() -} - -func (fsys *FS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { - return fsys.fs.WalkDir(root, walkFn) -} - -func (fsys *FS) WriteFile(path string, data string, writeByteOrderMark bool) error { - return fsys.fs.WriteFile(path, data, writeByteOrderMark) -} diff --git a/kitcom/internal/tsgo/vfs/internal/internal.go b/kitcom/internal/tsgo/vfs/internal/internal.go deleted file mode 100644 index 619c1d9..0000000 --- a/kitcom/internal/tsgo/vfs/internal/internal.go +++ /dev/null @@ -1,189 +0,0 @@ -package internal - -import ( - "encoding/binary" - "fmt" - "io/fs" - "strings" - "unicode/utf16" - "unsafe" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" -) - -type Common struct { - RootFor func(root string) fs.FS - Realpath func(path string) string -} - -func RootLength(p string) int { - l := tspath.GetEncodedRootLength(p) - if l <= 0 { - panic(fmt.Sprintf("vfs: path %q is not absolute", p)) - } - return l -} - -func SplitPath(p string) (rootName, rest string) { - p = tspath.NormalizePath(p) - l := RootLength(p) - rootName, rest = p[:l], p[l:] - rest = tspath.RemoveTrailingDirectorySeparator(rest) - return rootName, rest -} - -func (vfs *Common) RootAndPath(path string) (fsys fs.FS, rootName string, rest string) { - rootName, rest = SplitPath(path) - if rest == "" { - rest = "." - } - return vfs.RootFor(rootName), rootName, rest -} - -func (vfs *Common) Stat(path string) vfs.FileInfo { - fsys, _, rest := vfs.RootAndPath(path) - if fsys == nil { - return nil - } - stat, err := fs.Stat(fsys, rest) - if err != nil { - return nil - } - return stat -} - -func (vfs *Common) FileExists(path string) bool { - stat := vfs.Stat(path) - return stat != nil && !stat.IsDir() -} - -func (vfs *Common) DirectoryExists(path string) bool { - stat := vfs.Stat(path) - return stat != nil && stat.IsDir() -} - -func (vfs *Common) GetAccessibleEntries(path string) (result vfs.Entries) { - addToResult := func(name string, mode fs.FileMode) (added bool) { - if mode.IsDir() { - result.Directories = append(result.Directories, name) - return true - } - - if mode.IsRegular() { - result.Files = append(result.Files, name) - return true - } - - return false - } - - for _, entry := range vfs.getEntries(path) { - entryType := entry.Type() - - if addToResult(entry.Name(), entryType) { - continue - } - - if entryType&fs.ModeSymlink != 0 { - // Easy case; UNIX-like system will clearly mark symlinks. - if stat := vfs.Stat(path + "/" + entry.Name()); stat != nil { - addToResult(entry.Name(), stat.Mode()) - } - continue - } - - if entryType&fs.ModeIrregular != 0 && vfs.Realpath != nil { - // Could be a Windows junction. Try Realpath. - // TODO(jakebailey): use syscall.Win32FileAttributeData instead - fullPath := path + "/" + entry.Name() - if realpath := vfs.Realpath(fullPath); fullPath != realpath { - if stat := vfs.Stat(realpath); stat != nil { - addToResult(entry.Name(), stat.Mode()) - } - } - continue - } - } - - return result -} - -func (vfs *Common) getEntries(path string) []vfs.DirEntry { - fsys, _, rest := vfs.RootAndPath(path) - if fsys == nil { - return nil - } - - entries, err := fs.ReadDir(fsys, rest) - if err != nil { - return nil - } - - return entries -} - -func (vfs *Common) WalkDir(root string, walkFn fs.WalkDirFunc) error { - fsys, rootName, rest := vfs.RootAndPath(root) - if fsys == nil { - return nil - } - return fs.WalkDir(fsys, rest, func(path string, d fs.DirEntry, err error) error { - if path == "." { - path = "" - } - return walkFn(rootName+path, d, err) - }) -} - -func (vfs *Common) ReadFile(path string) (contents string, ok bool) { - fsys, _, rest := vfs.RootAndPath(path) - if fsys == nil { - return "", false - } - - b, err := fs.ReadFile(fsys, rest) - if err != nil { - return "", false - } - - // An invariant of any underlying filesystem is that the bytes returned - // are immutable, otherwise anyone using the filesystem would end up - // with data races. - // - // This means that we can safely convert the bytes to a string directly, - // saving a copy. - if len(b) == 0 { - return "", true - } - - s := unsafe.String(&b[0], len(b)) - - return decodeBytes(s) -} - -func decodeBytes(s string) (contents string, ok bool) { - var bom [2]byte - if len(s) >= 2 { - bom = [2]byte{s[0], s[1]} - switch bom { - case [2]byte{0xFF, 0xFE}: - return decodeUtf16(s[2:], binary.LittleEndian), true - case [2]byte{0xFE, 0xFF}: - return decodeUtf16(s[2:], binary.BigEndian), true - } - } - if len(s) >= 3 && s[0] == 0xEF && s[1] == 0xBB && s[2] == 0xBF { - s = s[3:] - } - - return s, true -} - -func decodeUtf16(s string, order binary.ByteOrder) string { - ints := make([]uint16, len(s)/2) - if err := binary.Read(strings.NewReader(s), order, &ints); err != nil { - return "" - } - return string(utf16.Decode(ints)) -} diff --git a/kitcom/internal/tsgo/vfs/iovfs/iofs.go b/kitcom/internal/tsgo/vfs/iovfs/iofs.go deleted file mode 100644 index a8ba814..0000000 --- a/kitcom/internal/tsgo/vfs/iovfs/iofs.go +++ /dev/null @@ -1,207 +0,0 @@ -package iovfs - -import ( - "fmt" - "io/fs" - "strings" - "time" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs/internal" -) - -type RealpathFS interface { - fs.FS - Realpath(path string) (string, error) -} - -type WritableFS interface { - fs.FS - WriteFile(path string, data []byte, perm fs.FileMode) error - MkdirAll(path string, perm fs.FileMode) error - // Removes `path` and all its contents. Will return the first error it encounters. - Remove(path string) error - Chtimes(path string, aTime time.Time, mTime time.Time) error -} - -type FsWithSys interface { - vfs.FS - FSys() fs.FS -} - -// From creates a new FS from an [fs.FS]. -// -// For paths like `c:/foo/bar`, fsys will be used as though it's rooted at `/` and the path is `/c:/foo/bar`. -// -// If the provided [fs.FS] implements [RealpathFS], it will be used to implement the Realpath method. -// If the provided [fs.FS] implements [WritableFS], it will be used to implement the WriteFile method. -// -// From does not actually handle case-insensitivity; ensure the passed in [fs.FS] -// respects case-insensitive file names if needed. Consider using [vfstest.FromMap] for testing. -func From(fsys fs.FS, useCaseSensitiveFileNames bool) FsWithSys { - var realpath func(path string) (string, error) - if fsys, ok := fsys.(RealpathFS); ok { - realpath = func(path string) (string, error) { - rest, hadSlash := strings.CutPrefix(path, "/") - rp, err := fsys.Realpath(rest) - if err != nil { - return "", err - } - if hadSlash { - return "/" + rp, nil - } - return rp, nil - } - } else { - realpath = func(path string) (string, error) { - return path, nil - } - } - - var writeFile func(path string, content string, writeByteOrderMark bool) error - var mkdirAll func(path string) error - var remove func(path string) error - var chtimes func(path string, aTime time.Time, mTime time.Time) error - if fsys, ok := fsys.(WritableFS); ok { - writeFile = func(path string, content string, writeByteOrderMark bool) error { - rest, _ := strings.CutPrefix(path, "/") - if writeByteOrderMark { - // Strada uses \uFEFF because NodeJS requires it, but substitutes it with the correct BOM based on the - // output encoding. \uFEFF is actually the BOM for big-endian UTF-16. For UTF-8 the actual BOM is - // \xEF\xBB\xBF. - content = stringutil.AddUTF8ByteOrderMark(content) - } - return fsys.WriteFile(rest, []byte(content), 0o666) - } - mkdirAll = func(path string) error { - rest, _ := strings.CutPrefix(path, "/") - return fsys.MkdirAll(rest, 0o777) - } - remove = func(path string) error { - rest, _ := strings.CutPrefix(path, "/") - return fsys.Remove(rest) - } - chtimes = func(path string, aTime time.Time, mTime time.Time) error { - rest, _ := strings.CutPrefix(path, "/") - return fsys.Chtimes(rest, aTime, mTime) - } - } else { - writeFile = func(string, string, bool) error { - panic("writeFile not supported") - } - mkdirAll = func(string) error { - panic("mkdirAll not supported") - } - remove = func(string) error { - panic("remove not supported") - } - chtimes = func(string, time.Time, time.Time) error { - panic("chtimes not supported") - } - } - - return &ioFS{ - common: internal.Common{ - RootFor: func(root string) fs.FS { - if root == "/" { - return fsys - } - - p := tspath.RemoveTrailingDirectorySeparator(root) - sub, err := fs.Sub(fsys, p) - if err != nil { - panic(fmt.Sprintf("vfs: failed to create sub file system for %q: %v", p, err)) - } - return sub - }, - }, - useCaseSensitiveFileNames: useCaseSensitiveFileNames, - realpath: realpath, - writeFile: writeFile, - mkdirAll: mkdirAll, - remove: remove, - chtimes: chtimes, - fsys: fsys, - } -} - -type ioFS struct { - common internal.Common - - useCaseSensitiveFileNames bool - realpath func(path string) (string, error) - writeFile func(path string, content string, writeByteOrderMark bool) error - mkdirAll func(path string) error - remove func(path string) error - chtimes func(path string, aTime time.Time, mTime time.Time) error - fsys fs.FS -} - -var _ FsWithSys = (*ioFS)(nil) - -func (vfs *ioFS) UseCaseSensitiveFileNames() bool { - return vfs.useCaseSensitiveFileNames -} - -func (vfs *ioFS) DirectoryExists(path string) bool { - return vfs.common.DirectoryExists(path) -} - -func (vfs *ioFS) FileExists(path string) bool { - return vfs.common.FileExists(path) -} - -func (vfs *ioFS) GetAccessibleEntries(path string) vfs.Entries { - return vfs.common.GetAccessibleEntries(path) -} - -func (vfs *ioFS) Stat(path string) vfs.FileInfo { - _ = internal.RootLength(path) // Assert path is rooted - return vfs.common.Stat(path) -} - -func (vfs *ioFS) ReadFile(path string) (contents string, ok bool) { - return vfs.common.ReadFile(path) -} - -func (vfs *ioFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { - return vfs.common.WalkDir(root, walkFn) -} - -func (vfs *ioFS) Remove(path string) error { - _ = internal.RootLength(path) // Assert path is rooted - return vfs.remove(path) -} - -func (vfs *ioFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { - _ = internal.RootLength(path) // Assert path is rooted - return vfs.chtimes(path, aTime, mTime) -} - -func (vfs *ioFS) Realpath(path string) string { - root, rest := internal.SplitPath(path) - // splitPath normalizes the path into parts (e.g. "c:/foo/bar" -> "c:/", "foo/bar") - // Put them back together to call realpath. - realpath, err := vfs.realpath(root + rest) - if err != nil { - return path - } - return realpath -} - -func (vfs *ioFS) WriteFile(path string, content string, writeByteOrderMark bool) error { - _ = internal.RootLength(path) // Assert path is rooted - if err := vfs.writeFile(path, content, writeByteOrderMark); err == nil { - return nil - } - if err := vfs.mkdirAll(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil { - return err - } - return vfs.writeFile(path, content, writeByteOrderMark) -} - -func (vfs *ioFS) FSys() fs.FS { - return vfs.fsys -} diff --git a/kitcom/internal/tsgo/vfs/osvfs/os.go b/kitcom/internal/tsgo/vfs/osvfs/os.go deleted file mode 100644 index 2185779..0000000 --- a/kitcom/internal/tsgo/vfs/osvfs/os.go +++ /dev/null @@ -1,177 +0,0 @@ -package osvfs - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "time" - "unicode" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs/internal" -) - -// FS creates a new FS from the OS file system. -func FS() vfs.FS { - return osVFS -} - -var osVFS vfs.FS = &osFS{ - common: internal.Common{ - RootFor: os.DirFS, - Realpath: osFSRealpath, - }, -} - -type osFS struct { - common internal.Common -} - -// We do this right at startup to minimize the chance that executable gets moved or deleted. -var isFileSystemCaseSensitive = func() bool { - // win32/win64 are case insensitive platforms - if runtime.GOOS == "windows" { - return false - } - - if runtime.GOARCH == "wasm" { - // !!! Who knows; this depends on the host implementation. - return true - } - - // As a proxy for case-insensitivity, we check if the current executable exists under a different case. - // This is not entirely correct, since different OSs can have differing case sensitivity in different paths, - // but this is largely good enough for our purposes (and what sys.ts used to do with __filename). - exe, err := os.Executable() - if err != nil { - panic(fmt.Sprintf("vfs: failed to get executable path: %v", err)) - } - - // If the current executable exists under a different case, we must be case-insensitive. - swapped := swapCase(exe) - if _, err := os.Stat(swapped); err != nil { - if os.IsNotExist(err) { - return true - } - panic(fmt.Sprintf("vfs: failed to stat %q: %v", swapped, err)) - } - return false -}() - -// Convert all lowercase chars to uppercase, and vice-versa -func swapCase(str string) string { - return strings.Map(func(r rune) rune { - upper := unicode.ToUpper(r) - if upper == r { - return unicode.ToLower(r) - } else { - return upper - } - }, str) -} - -func (vfs *osFS) UseCaseSensitiveFileNames() bool { - return isFileSystemCaseSensitive -} - -var readSema = make(chan struct{}, 128) - -func (vfs *osFS) ReadFile(path string) (contents string, ok bool) { - // Limit ourselves to fewer open files, which greatly reduces IO contention. - readSema <- struct{}{} - defer func() { <-readSema }() - - return vfs.common.ReadFile(path) -} - -func (vfs *osFS) DirectoryExists(path string) bool { - return vfs.common.DirectoryExists(path) -} - -func (vfs *osFS) FileExists(path string) bool { - return vfs.common.FileExists(path) -} - -func (vfs *osFS) GetAccessibleEntries(path string) vfs.Entries { - return vfs.common.GetAccessibleEntries(path) -} - -func (vfs *osFS) Stat(path string) vfs.FileInfo { - return vfs.common.Stat(path) -} - -func (vfs *osFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { - return vfs.common.WalkDir(root, walkFn) -} - -func (vfs *osFS) Realpath(path string) string { - return osFSRealpath(path) -} - -func osFSRealpath(path string) string { - _ = internal.RootLength(path) // Assert path is rooted - - orig := path - path = filepath.FromSlash(path) - path, err := realpath(path) - if err != nil { - return orig - } - path, err = filepath.Abs(path) - if err != nil { - return orig - } - return tspath.NormalizeSlashes(path) -} - -var writeSema = make(chan struct{}, 32) - -func (vfs *osFS) writeFile(path string, content string, writeByteOrderMark bool) error { - writeSema <- struct{}{} - defer func() { <-writeSema }() - - file, err := os.Create(path) - if err != nil { - return err - } - defer file.Close() - - if writeByteOrderMark { - if _, err := file.WriteString("\uFEFF"); err != nil { - return err - } - } - - if _, err := file.WriteString(content); err != nil { - return err - } - - return nil -} - -func (vfs *osFS) ensureDirectoryExists(directoryPath string) error { - return os.MkdirAll(directoryPath, 0o777) -} - -func (vfs *osFS) WriteFile(path string, content string, writeByteOrderMark bool) error { - _ = internal.RootLength(path) // Assert path is rooted - if err := vfs.writeFile(path, content, writeByteOrderMark); err == nil { - return nil - } - if err := vfs.ensureDirectoryExists(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil { - return err - } - return vfs.writeFile(path, content, writeByteOrderMark) -} - -func (vfs *osFS) Remove(path string) error { - // todo: #701 add retry mechanism? - return os.RemoveAll(path) -} - -func (vfs *osFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { - return os.Chtimes(path, aTime, mTime) -} diff --git a/kitcom/internal/tsgo/vfs/osvfs/realpath_other.go b/kitcom/internal/tsgo/vfs/osvfs/realpath_other.go deleted file mode 100644 index 5562e30..0000000 --- a/kitcom/internal/tsgo/vfs/osvfs/realpath_other.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !windows - -package osvfs - -import ( - "path/filepath" -) - -func realpath(path string) (string, error) { - return filepath.EvalSymlinks(path) -} diff --git a/kitcom/internal/tsgo/vfs/osvfs/realpath_windows.go b/kitcom/internal/tsgo/vfs/osvfs/realpath_windows.go deleted file mode 100644 index 77b2d5a..0000000 --- a/kitcom/internal/tsgo/vfs/osvfs/realpath_windows.go +++ /dev/null @@ -1,100 +0,0 @@ -package osvfs - -import ( - "errors" - "os" - "syscall" - - "golang.org/x/sys/windows" -) - -// This implementation is based on what Node's fs.realpath.native does, via libuv: https://github.com/libuv/libuv/blob/ec5a4b54f7da7eeb01679005c615fee9633cdb3b/src/win/fs.c#L2937 - -func realpath(path string) (string, error) { - var h windows.Handle - if len(path) < 248 { - var err error - h, err = openMetadata(path) - if err != nil { - return "", err - } - defer windows.CloseHandle(h) //nolint:errcheck - } else { - // For long paths, defer to os.Open to run the path through fixLongPath. - f, err := os.Open(path) - if err != nil { - return "", err - } - defer f.Close() - - // Works on directories too since https://go.dev/cl/405275. - h = windows.Handle(f.Fd()) - } - - // based on https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/os/file_windows.go#L389 - - const _VOLUME_NAME_DOS = 0 - - buf := make([]uint16, 310) // https://github.com/microsoft/go-winio/blob/3c9576c9346a1892dee136329e7e15309e82fb4f/internal/stringbuffer/wstring.go#L13 - for { - n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), _VOLUME_NAME_DOS) - if err != nil { - return "", err - } - if n < uint32(len(buf)) { - break - } - buf = make([]uint16, n) - } - - s := syscall.UTF16ToString(buf) - if len(s) > 4 && s[:4] == `\\?\` { - s = s[4:] - if len(s) > 3 && s[:3] == `UNC` { - // return path like \\server\share\... - return `\` + s[3:], nil - } - return s, nil - } - - return "", errors.New("GetFinalPathNameByHandle returned unexpected path: " + s) -} - -func openMetadata(path string) (windows.Handle, error) { - // based on https://github.com/microsoft/go-winio/blob/3c9576c9346a1892dee136329e7e15309e82fb4f/pkg/fs/resolve.go#L113 - - pathUTF16, err := windows.UTF16PtrFromString(path) - if err != nil { - return windows.InvalidHandle, err - } - - const ( - _FILE_ANY_ACCESS = 0 - - _FILE_SHARE_READ = 0x01 - _FILE_SHARE_WRITE = 0x02 - _FILE_SHARE_DELETE = 0x04 - - _OPEN_EXISTING = 0x03 - - _FILE_FLAG_BACKUP_SEMANTICS = 0x0200_0000 - ) - - h, err := windows.CreateFile( - pathUTF16, - _FILE_ANY_ACCESS, - _FILE_SHARE_READ|_FILE_SHARE_WRITE|_FILE_SHARE_DELETE, - nil, - _OPEN_EXISTING, - _FILE_FLAG_BACKUP_SEMANTICS, - 0, - ) - if err != nil { - return 0, &os.PathError{ - Op: "CreateFile", - Path: path, - Err: err, - } - } - return h, nil -} diff --git a/kitcom/internal/tsgo/vfs/utilities.go b/kitcom/internal/tsgo/vfs/utilities.go deleted file mode 100644 index 81822f9..0000000 --- a/kitcom/internal/tsgo/vfs/utilities.go +++ /dev/null @@ -1,464 +0,0 @@ -package vfs - -import ( - "fmt" - "regexp" - "sort" - "strings" - "sync" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" - "github.com/dlclark/regexp2" -) - -type FileMatcherPatterns struct { - // One pattern for each "include" spec. - includeFilePatterns []string - // One pattern matching one of any of the "include" specs. - includeFilePattern string - includeDirectoryPattern string - excludePattern string - basePaths []string -} - -type usage string - -const ( - usageFiles usage = "files" - usageDirectories usage = "directories" - usageExclude usage = "exclude" -) - -func GetRegularExpressionsForWildcards(specs []string, basePath string, usage usage) []string { - if len(specs) == 0 { - return nil - } - return core.Map(specs, func(spec string) string { - return getSubPatternFromSpec(spec, basePath, usage, wildcardMatchers[usage]) - }) -} - -func GetRegularExpressionForWildcard(specs []string, basePath string, usage usage) string { - patterns := GetRegularExpressionsForWildcards(specs, basePath, usage) - if len(patterns) == 0 { - return "" - } - - mappedPatterns := make([]string, len(patterns)) - for i, pattern := range patterns { - mappedPatterns[i] = fmt.Sprintf("(%s)", pattern) - } - pattern := strings.Join(mappedPatterns, "|") - - // If excluding, match "foo/bar/baz...", but if including, only allow "foo". - var terminator string - if usage == "exclude" { - terminator = "($|/)" - } else { - terminator = "$" - } - return fmt.Sprintf("^(%s)%s", pattern, terminator) -} - -func replaceWildcardCharacter(match string, singleAsteriskRegexFragment string) string { - if match == "*" { - return singleAsteriskRegexFragment - } else { - if match == "?" { - return "[^/]" - } else { - return "\\" + match - } - } -} - -// An "includes" path "foo" is implicitly a glob "foo/** /*" (without the space) if its last component has no extension, -// and does not contain any glob characters itself. -func IsImplicitGlob(lastPathComponent string) bool { - return !strings.ContainsAny(lastPathComponent, ".*?") -} - -// Reserved characters, forces escaping of any non-word (or digit), non-whitespace character. -// It may be inefficient (we could just match (/[-[\]{}()*+?.,\\^$|#\s]/g), but this is future -// proof. -var ( - reservedCharacterPattern *regexp.Regexp = regexp.MustCompile(`[^\w\s/]`) - wildcardCharCodes = []rune{'*', '?'} -) - -var ( - commonPackageFolders = []string{"node_modules", "bower_components", "jspm_packages"} - implicitExcludePathRegexPattern = "(?!(" + strings.Join(commonPackageFolders, "|") + ")(/|$))" -) - -type WildcardMatcher struct { - singleAsteriskRegexFragment string - doubleAsteriskRegexFragment string - replaceWildcardCharacter func(match string) string -} - -const ( - // Matches any single directory segment unless it is the last segment and a .min.js file - // Breakdown: - // - // [^./] # matches everything up to the first . character (excluding directory separators) - // (\\.(?!min\\.js$))? # matches . characters but not if they are part of the .min.js file extension - singleAsteriskRegexFragmentFilesMatcher = "([^./]|(\\.(?!min\\.js$))?)*" - singleAsteriskRegexFragment = "[^/]*" -) - -var filesMatcher = WildcardMatcher{ - singleAsteriskRegexFragment: singleAsteriskRegexFragmentFilesMatcher, - // Regex for the ** wildcard. Matches any number of subdirectories. When used for including - // files or directories, does not match subdirectories that start with a . character - doubleAsteriskRegexFragment: "(/" + implicitExcludePathRegexPattern + "[^/.][^/]*)*?", - replaceWildcardCharacter: func(match string) string { - return replaceWildcardCharacter(match, singleAsteriskRegexFragmentFilesMatcher) - }, -} - -var directoriesMatcher = WildcardMatcher{ - singleAsteriskRegexFragment: singleAsteriskRegexFragment, - // Regex for the ** wildcard. Matches any number of subdirectories. When used for including - // files or directories, does not match subdirectories that start with a . character - doubleAsteriskRegexFragment: "(/" + implicitExcludePathRegexPattern + "[^/.][^/]*)*?", - replaceWildcardCharacter: func(match string) string { - return replaceWildcardCharacter(match, singleAsteriskRegexFragment) - }, -} - -var excludeMatcher = WildcardMatcher{ - singleAsteriskRegexFragment: singleAsteriskRegexFragment, - doubleAsteriskRegexFragment: "(/.+?)?", - replaceWildcardCharacter: func(match string) string { - return replaceWildcardCharacter(match, singleAsteriskRegexFragment) - }, -} - -var wildcardMatchers = map[usage]WildcardMatcher{ - usageFiles: filesMatcher, - usageDirectories: directoriesMatcher, - usageExclude: excludeMatcher, -} - -func GetPatternFromSpec( - spec string, - basePath string, - usage usage, -) string { - pattern := getSubPatternFromSpec(spec, basePath, usage, wildcardMatchers[usage]) - if pattern == "" { - return "" - } - ending := core.IfElse(usage == "exclude", "($|/)", "$") - return fmt.Sprintf("^(%s)%s", pattern, ending) -} - -func getSubPatternFromSpec( - spec string, - basePath string, - usage usage, - matcher WildcardMatcher, -) string { - matcher = wildcardMatchers[usage] - - replaceWildcardCharacter := matcher.replaceWildcardCharacter - - var subpattern strings.Builder - hasWrittenComponent := false - components := tspath.GetNormalizedPathComponents(spec, basePath) - lastComponent := core.LastOrNil(components) - if usage != "exclude" && lastComponent == "**" { - return "" - } - - // getNormalizedPathComponents includes the separator for the root component. - // We need to remove to create our regex correctly. - components[0] = tspath.RemoveTrailingDirectorySeparator(components[0]) - - if IsImplicitGlob(lastComponent) { - components = append(components, "**", "*") - } - - optionalCount := 0 - for _, component := range components { - if component == "**" { - subpattern.WriteString(matcher.doubleAsteriskRegexFragment) - } else { - if usage == "directories" { - subpattern.WriteString("(") - optionalCount++ - } - - if hasWrittenComponent { - subpattern.WriteRune(tspath.DirectorySeparator) - } - - if usage != "exclude" { - var componentPattern strings.Builder - if strings.HasPrefix(component, "*") { - componentPattern.WriteString("([^./]" + matcher.singleAsteriskRegexFragment + ")?") - component = component[1:] - } else if strings.HasPrefix(component, "?") { - componentPattern.WriteString("[^./]") - component = component[1:] - } - componentPattern.WriteString(reservedCharacterPattern.ReplaceAllStringFunc(component, replaceWildcardCharacter)) - - // Patterns should not include subfolders like node_modules unless they are - // explicitly included as part of the path. - // - // As an optimization, if the component pattern is the same as the component, - // then there definitely were no wildcard characters and we do not need to - // add the exclusion pattern. - if componentPattern.String() != component { - subpattern.WriteString(implicitExcludePathRegexPattern) - } - subpattern.WriteString(componentPattern.String()) - } else { - subpattern.WriteString(reservedCharacterPattern.ReplaceAllStringFunc(component, replaceWildcardCharacter)) - } - } - hasWrittenComponent = true - } - - for optionalCount > 0 { - subpattern.WriteString(")?") - optionalCount-- - } - - return subpattern.String() -} - -func getIncludeBasePath(absolute string) string { - wildcardOffset := strings.IndexAny(absolute, string(wildcardCharCodes)) - if wildcardOffset < 0 { - // No "*" or "?" in the path - if !tspath.HasExtension(absolute) { - return absolute - } else { - return tspath.RemoveTrailingDirectorySeparator(tspath.GetDirectoryPath(absolute)) - } - } - return absolute[:max(strings.LastIndex(absolute[:wildcardOffset], string(tspath.DirectorySeparator)), 0)] -} - -// getBasePaths computes the unique non-wildcard base paths amongst the provided include patterns. -func getBasePaths(path string, includes []string, useCaseSensitiveFileNames bool) []string { - // Storage for our results in the form of literal paths (e.g. the paths as written by the user). - basePaths := []string{path} - - if len(includes) > 0 { - // Storage for literal base paths amongst the include patterns. - includeBasePaths := []string{} - for _, include := range includes { - // We also need to check the relative paths by converting them to absolute and normalizing - // in case they escape the base path (e.g "..\somedirectory") - var absolute string - if tspath.IsRootedDiskPath(include) { - absolute = include - } else { - absolute = tspath.NormalizePath(tspath.CombinePaths(path, include)) - } - // Append the literal and canonical candidate base paths. - includeBasePaths = append(includeBasePaths, getIncludeBasePath(absolute)) - } - - // Sort the offsets array using either the literal or canonical path representations. - stringComparer := stringutil.GetStringComparer(!useCaseSensitiveFileNames) - sort.SliceStable(includeBasePaths, func(i, j int) bool { - return stringComparer(includeBasePaths[i], includeBasePaths[j]) < 0 - }) - - // Iterate over each include base path and include unique base paths that are not a - // subpath of an existing base path - for _, includeBasePath := range includeBasePaths { - if core.Every(basePaths, func(basepath string) bool { - return !tspath.ContainsPath(basepath, includeBasePath, tspath.ComparePathsOptions{CurrentDirectory: path, UseCaseSensitiveFileNames: !useCaseSensitiveFileNames}) - }) { - basePaths = append(basePaths, includeBasePath) - } - } - } - - return basePaths -} - -// getFileMatcherPatterns generates file matching patterns based on the provided path, -// includes, excludes, and other parameters. path is the directory of the tsconfig.json file. -func getFileMatcherPatterns(path string, excludes []string, includes []string, useCaseSensitiveFileNames bool, currentDirectory string) FileMatcherPatterns { - path = tspath.NormalizePath(path) - currentDirectory = tspath.NormalizePath(currentDirectory) - absolutePath := tspath.CombinePaths(currentDirectory, path) - - return FileMatcherPatterns{ - includeFilePatterns: core.Map(GetRegularExpressionsForWildcards(includes, absolutePath, "files"), func(pattern string) string { return "^" + pattern + "$" }), - includeFilePattern: GetRegularExpressionForWildcard(includes, absolutePath, "files"), - includeDirectoryPattern: GetRegularExpressionForWildcard(includes, absolutePath, "directories"), - excludePattern: GetRegularExpressionForWildcard(excludes, absolutePath, "exclude"), - basePaths: getBasePaths(path, includes, useCaseSensitiveFileNames), - } -} - -type regexp2CacheKey struct { - pattern string - opts regexp2.RegexOptions -} - -var ( - regexp2CacheMu sync.RWMutex - regexp2Cache = make(map[regexp2CacheKey]*regexp2.Regexp) -) - -func GetRegexFromPattern(pattern string, useCaseSensitiveFileNames bool) *regexp2.Regexp { - flags := regexp2.ECMAScript - if !useCaseSensitiveFileNames { - flags |= regexp2.IgnoreCase - } - opts := regexp2.RegexOptions(flags) - - key := regexp2CacheKey{pattern, opts} - - regexp2CacheMu.RLock() - re, ok := regexp2Cache[key] - regexp2CacheMu.RUnlock() - if ok { - return re - } - - regexp2CacheMu.Lock() - defer regexp2CacheMu.Unlock() - - re, ok = regexp2Cache[key] - if ok { - return re - } - - // Avoid infinite growth; may cause thrashing but no worse than not caching at all. - if len(regexp2Cache) > 1000 { - clear(regexp2Cache) - } - - // Avoid holding onto the pattern string, since this may pin a full config file in memory. - pattern = strings.Clone(pattern) - key.pattern = pattern - - re = regexp2.MustCompile(pattern, opts) - regexp2Cache[key] = re - return re -} - -type visitor struct { - includeFileRegexes []*regexp2.Regexp - excludeRegex *regexp2.Regexp - includeDirectoryRegex *regexp2.Regexp - extensions []string - useCaseSensitiveFileNames bool - host FS - visited collections.Set[string] - results [][]string -} - -func (v *visitor) visitDirectory( - path string, - absolutePath string, - depth *int, -) { - canonicalPath := tspath.GetCanonicalFileName(absolutePath, v.useCaseSensitiveFileNames) - if v.visited.Has(canonicalPath) { - return - } - v.visited.Add(canonicalPath) - systemEntries := v.host.GetAccessibleEntries(absolutePath) - files := systemEntries.Files - directories := systemEntries.Directories - - for _, current := range files { - name := tspath.CombinePaths(path, current) - absoluteName := tspath.CombinePaths(absolutePath, current) - if len(v.extensions) > 0 && !tspath.FileExtensionIsOneOf(name, v.extensions) { - continue - } - if v.excludeRegex != nil && core.Must(v.excludeRegex.MatchString(absoluteName)) { - continue - } - if v.includeFileRegexes == nil { - (v.results)[0] = append((v.results)[0], name) - } else { - includeIndex := core.FindIndex(v.includeFileRegexes, func(re *regexp2.Regexp) bool { return core.Must(re.MatchString(absoluteName)) }) - if includeIndex != -1 { - (v.results)[includeIndex] = append((v.results)[includeIndex], name) - } - } - } - - if depth != nil { - newDepth := *depth - 1 - if newDepth == 0 { - return - } - depth = &newDepth - } - - for _, current := range directories { - name := tspath.CombinePaths(path, current) - absoluteName := tspath.CombinePaths(absolutePath, current) - if (v.includeDirectoryRegex == nil || core.Must(v.includeDirectoryRegex.MatchString(absoluteName))) && (v.excludeRegex == nil || !core.Must(v.excludeRegex.MatchString(absoluteName))) { - v.visitDirectory(name, absoluteName, depth) - } - } -} - -// path is the directory of the tsconfig.json -func matchFiles(path string, extensions []string, excludes []string, includes []string, useCaseSensitiveFileNames bool, currentDirectory string, depth *int, host FS) []string { - path = tspath.NormalizePath(path) - currentDirectory = tspath.NormalizePath(currentDirectory) - - patterns := getFileMatcherPatterns(path, excludes, includes, useCaseSensitiveFileNames, currentDirectory) - var includeFileRegexes []*regexp2.Regexp - if patterns.includeFilePatterns != nil { - includeFileRegexes = core.Map(patterns.includeFilePatterns, func(pattern string) *regexp2.Regexp { return GetRegexFromPattern(pattern, useCaseSensitiveFileNames) }) - } - var includeDirectoryRegex *regexp2.Regexp - if patterns.includeDirectoryPattern != "" { - includeDirectoryRegex = GetRegexFromPattern(patterns.includeDirectoryPattern, useCaseSensitiveFileNames) - } - var excludeRegex *regexp2.Regexp - if patterns.excludePattern != "" { - excludeRegex = GetRegexFromPattern(patterns.excludePattern, useCaseSensitiveFileNames) - } - - // Associate an array of results with each include regex. This keeps results in order of the "include" order. - // If there are no "includes", then just put everything in results[0]. - var results [][]string - if len(includeFileRegexes) > 0 { - tempResults := make([][]string, len(includeFileRegexes)) - for i := range includeFileRegexes { - tempResults[i] = []string{} - } - results = tempResults - } else { - results = [][]string{{}} - } - v := visitor{ - useCaseSensitiveFileNames: useCaseSensitiveFileNames, - host: host, - includeFileRegexes: includeFileRegexes, - excludeRegex: excludeRegex, - includeDirectoryRegex: includeDirectoryRegex, - extensions: extensions, - results: results, - } - for _, basePath := range patterns.basePaths { - v.visitDirectory(basePath, tspath.CombinePaths(currentDirectory, basePath), depth) - } - - return core.Flatten(results) -} - -func ReadDirectory(host FS, currentDir string, path string, extensions []string, excludes []string, includes []string, depth *int) []string { - return matchFiles(path, extensions, excludes, includes, host.UseCaseSensitiveFileNames(), currentDir, depth, host) -} diff --git a/kitcom/internal/tsgo/vfs/vfs.go b/kitcom/internal/tsgo/vfs/vfs.go deleted file mode 100644 index 559fe47..0000000 --- a/kitcom/internal/tsgo/vfs/vfs.go +++ /dev/null @@ -1,79 +0,0 @@ -package vfs - -import ( - "io/fs" - "time" -) - -//go:generate go tool github.com/matryer/moq -fmt goimports -out vfsmock/mock_generated.go -pkg vfsmock . FS -//go:generate go tool mvdan.cc/gofumpt -w vfsmock/mock_generated.go - -// FS is a file system abstraction. -type FS interface { - // UseCaseSensitiveFileNames returns true if the file system is case-sensitive. - UseCaseSensitiveFileNames() bool - - // FileExists returns true if the file exists. - FileExists(path string) bool - - // ReadFile reads the file specified by path and returns the content. - // If the file fails to be read, ok will be false. - ReadFile(path string) (contents string, ok bool) - - WriteFile(path string, data string, writeByteOrderMark bool) error - - // Removes `path` and all its contents. Will return the first error it encounters. - Remove(path string) error - - // Chtimes changes the access and modification times of the named - Chtimes(path string, aTime time.Time, mTime time.Time) error - - // DirectoryExists returns true if the path is a directory. - DirectoryExists(path string) bool - - // GetAccessibleEntries returns the files/directories in the specified directory. - // If any entry is a symlink, it will be followed. - GetAccessibleEntries(path string) Entries - - Stat(path string) FileInfo - - // WalkDir walks the file tree rooted at root, calling walkFn for each file or directory in the tree. - // It is has the same behavior as [fs.WalkDir], but with paths as [string]. - WalkDir(root string, walkFn WalkDirFunc) error - - // Realpath returns the "real path" of the specified path, - // following symlinks and correcting filename casing. - Realpath(path string) string -} - -type Entries struct { - Files []string - Directories []string -} - -type ( - // DirEntry is [fs.DirEntry]. - DirEntry = fs.DirEntry - - // FileInfo is [fs.FileInfo]. - FileInfo = fs.FileInfo -) - -var ( - ErrInvalid = fs.ErrInvalid // "invalid argument" - ErrPermission = fs.ErrPermission // "permission denied" - ErrExist = fs.ErrExist // "file already exists" - ErrNotExist = fs.ErrNotExist // "file does not exist" - ErrClosed = fs.ErrClosed // "file already closed" -) - -// WalkDirFunc is [fs.WalkDirFunc]. -type WalkDirFunc = fs.WalkDirFunc - -var ( - // SkipAll is [fs.SkipAll]. - SkipAll = fs.SkipAll //nolint:errname - - // SkipDir is [fs.SkipDir]. - SkipDir = fs.SkipDir //nolint:errname -) diff --git a/kitcom/internal/tsgo/vfs/vfsmock/mock_generated.go b/kitcom/internal/tsgo/vfs/vfsmock/mock_generated.go deleted file mode 100644 index 145ab73..0000000 --- a/kitcom/internal/tsgo/vfs/vfsmock/mock_generated.go +++ /dev/null @@ -1,536 +0,0 @@ -// Code generated by moq; DO NOT EDIT. -// github.com/matryer/moq - -package vfsmock - -import ( - "sync" - "time" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" -) - -// Ensure, that FSMock does implement vfs.FS. -// If this is not the case, regenerate this file with moq. -var _ vfs.FS = &FSMock{} - -// FSMock is a mock implementation of vfs.FS. -// -// func TestSomethingThatUsesFS(t *testing.T) { -// -// // make and configure a mocked vfs.FS -// mockedFS := &FSMock{ -// ChtimesFunc: func(path string, aTime time.Time, mTime time.Time) error { -// panic("mock out the Chtimes method") -// }, -// DirectoryExistsFunc: func(path string) bool { -// panic("mock out the DirectoryExists method") -// }, -// FileExistsFunc: func(path string) bool { -// panic("mock out the FileExists method") -// }, -// GetAccessibleEntriesFunc: func(path string) vfs.Entries { -// panic("mock out the GetAccessibleEntries method") -// }, -// ReadFileFunc: func(path string) (string, bool) { -// panic("mock out the ReadFile method") -// }, -// RealpathFunc: func(path string) string { -// panic("mock out the Realpath method") -// }, -// RemoveFunc: func(path string) error { -// panic("mock out the Remove method") -// }, -// StatFunc: func(path string) vfs.FileInfo { -// panic("mock out the Stat method") -// }, -// UseCaseSensitiveFileNamesFunc: func() bool { -// panic("mock out the UseCaseSensitiveFileNames method") -// }, -// WalkDirFunc: func(root string, walkFn vfs.WalkDirFunc) error { -// panic("mock out the WalkDir method") -// }, -// WriteFileFunc: func(path string, data string, writeByteOrderMark bool) error { -// panic("mock out the WriteFile method") -// }, -// } -// -// // use mockedFS in code that requires vfs.FS -// // and then make assertions. -// -// } -type FSMock struct { - // ChtimesFunc mocks the Chtimes method. - ChtimesFunc func(path string, aTime time.Time, mTime time.Time) error - - // DirectoryExistsFunc mocks the DirectoryExists method. - DirectoryExistsFunc func(path string) bool - - // FileExistsFunc mocks the FileExists method. - FileExistsFunc func(path string) bool - - // GetAccessibleEntriesFunc mocks the GetAccessibleEntries method. - GetAccessibleEntriesFunc func(path string) vfs.Entries - - // ReadFileFunc mocks the ReadFile method. - ReadFileFunc func(path string) (string, bool) - - // RealpathFunc mocks the Realpath method. - RealpathFunc func(path string) string - - // RemoveFunc mocks the Remove method. - RemoveFunc func(path string) error - - // StatFunc mocks the Stat method. - StatFunc func(path string) vfs.FileInfo - - // UseCaseSensitiveFileNamesFunc mocks the UseCaseSensitiveFileNames method. - UseCaseSensitiveFileNamesFunc func() bool - - // WalkDirFunc mocks the WalkDir method. - WalkDirFunc func(root string, walkFn vfs.WalkDirFunc) error - - // WriteFileFunc mocks the WriteFile method. - WriteFileFunc func(path string, data string, writeByteOrderMark bool) error - - // calls tracks calls to the methods. - calls struct { - // Chtimes holds details about calls to the Chtimes method. - Chtimes []struct { - // Path is the path argument value. - Path string - // ATime is the aTime argument value. - ATime time.Time - // MTime is the mTime argument value. - MTime time.Time - } - // DirectoryExists holds details about calls to the DirectoryExists method. - DirectoryExists []struct { - // Path is the path argument value. - Path string - } - // FileExists holds details about calls to the FileExists method. - FileExists []struct { - // Path is the path argument value. - Path string - } - // GetAccessibleEntries holds details about calls to the GetAccessibleEntries method. - GetAccessibleEntries []struct { - // Path is the path argument value. - Path string - } - // ReadFile holds details about calls to the ReadFile method. - ReadFile []struct { - // Path is the path argument value. - Path string - } - // Realpath holds details about calls to the Realpath method. - Realpath []struct { - // Path is the path argument value. - Path string - } - // Remove holds details about calls to the Remove method. - Remove []struct { - // Path is the path argument value. - Path string - } - // Stat holds details about calls to the Stat method. - Stat []struct { - // Path is the path argument value. - Path string - } - // UseCaseSensitiveFileNames holds details about calls to the UseCaseSensitiveFileNames method. - UseCaseSensitiveFileNames []struct{} - // WalkDir holds details about calls to the WalkDir method. - WalkDir []struct { - // Root is the root argument value. - Root string - // WalkFn is the walkFn argument value. - WalkFn vfs.WalkDirFunc - } - // WriteFile holds details about calls to the WriteFile method. - WriteFile []struct { - // Path is the path argument value. - Path string - // Data is the data argument value. - Data string - // WriteByteOrderMark is the writeByteOrderMark argument value. - WriteByteOrderMark bool - } - } - lockChtimes sync.RWMutex - lockDirectoryExists sync.RWMutex - lockFileExists sync.RWMutex - lockGetAccessibleEntries sync.RWMutex - lockReadFile sync.RWMutex - lockRealpath sync.RWMutex - lockRemove sync.RWMutex - lockStat sync.RWMutex - lockUseCaseSensitiveFileNames sync.RWMutex - lockWalkDir sync.RWMutex - lockWriteFile sync.RWMutex -} - -// Chtimes calls ChtimesFunc. -func (mock *FSMock) Chtimes(path string, aTime time.Time, mTime time.Time) error { - if mock.ChtimesFunc == nil { - panic("FSMock.ChtimesFunc: method is nil but FS.Chtimes was just called") - } - callInfo := struct { - Path string - ATime time.Time - MTime time.Time - }{ - Path: path, - ATime: aTime, - MTime: mTime, - } - mock.lockChtimes.Lock() - mock.calls.Chtimes = append(mock.calls.Chtimes, callInfo) - mock.lockChtimes.Unlock() - return mock.ChtimesFunc(path, aTime, mTime) -} - -// ChtimesCalls gets all the calls that were made to Chtimes. -// Check the length with: -// -// len(mockedFS.ChtimesCalls()) -func (mock *FSMock) ChtimesCalls() []struct { - Path string - ATime time.Time - MTime time.Time -} { - var calls []struct { - Path string - ATime time.Time - MTime time.Time - } - mock.lockChtimes.RLock() - calls = mock.calls.Chtimes - mock.lockChtimes.RUnlock() - return calls -} - -// DirectoryExists calls DirectoryExistsFunc. -func (mock *FSMock) DirectoryExists(path string) bool { - if mock.DirectoryExistsFunc == nil { - panic("FSMock.DirectoryExistsFunc: method is nil but FS.DirectoryExists was just called") - } - callInfo := struct { - Path string - }{ - Path: path, - } - mock.lockDirectoryExists.Lock() - mock.calls.DirectoryExists = append(mock.calls.DirectoryExists, callInfo) - mock.lockDirectoryExists.Unlock() - return mock.DirectoryExistsFunc(path) -} - -// DirectoryExistsCalls gets all the calls that were made to DirectoryExists. -// Check the length with: -// -// len(mockedFS.DirectoryExistsCalls()) -func (mock *FSMock) DirectoryExistsCalls() []struct { - Path string -} { - var calls []struct { - Path string - } - mock.lockDirectoryExists.RLock() - calls = mock.calls.DirectoryExists - mock.lockDirectoryExists.RUnlock() - return calls -} - -// FileExists calls FileExistsFunc. -func (mock *FSMock) FileExists(path string) bool { - if mock.FileExistsFunc == nil { - panic("FSMock.FileExistsFunc: method is nil but FS.FileExists was just called") - } - callInfo := struct { - Path string - }{ - Path: path, - } - mock.lockFileExists.Lock() - mock.calls.FileExists = append(mock.calls.FileExists, callInfo) - mock.lockFileExists.Unlock() - return mock.FileExistsFunc(path) -} - -// FileExistsCalls gets all the calls that were made to FileExists. -// Check the length with: -// -// len(mockedFS.FileExistsCalls()) -func (mock *FSMock) FileExistsCalls() []struct { - Path string -} { - var calls []struct { - Path string - } - mock.lockFileExists.RLock() - calls = mock.calls.FileExists - mock.lockFileExists.RUnlock() - return calls -} - -// GetAccessibleEntries calls GetAccessibleEntriesFunc. -func (mock *FSMock) GetAccessibleEntries(path string) vfs.Entries { - if mock.GetAccessibleEntriesFunc == nil { - panic("FSMock.GetAccessibleEntriesFunc: method is nil but FS.GetAccessibleEntries was just called") - } - callInfo := struct { - Path string - }{ - Path: path, - } - mock.lockGetAccessibleEntries.Lock() - mock.calls.GetAccessibleEntries = append(mock.calls.GetAccessibleEntries, callInfo) - mock.lockGetAccessibleEntries.Unlock() - return mock.GetAccessibleEntriesFunc(path) -} - -// GetAccessibleEntriesCalls gets all the calls that were made to GetAccessibleEntries. -// Check the length with: -// -// len(mockedFS.GetAccessibleEntriesCalls()) -func (mock *FSMock) GetAccessibleEntriesCalls() []struct { - Path string -} { - var calls []struct { - Path string - } - mock.lockGetAccessibleEntries.RLock() - calls = mock.calls.GetAccessibleEntries - mock.lockGetAccessibleEntries.RUnlock() - return calls -} - -// ReadFile calls ReadFileFunc. -func (mock *FSMock) ReadFile(path string) (string, bool) { - if mock.ReadFileFunc == nil { - panic("FSMock.ReadFileFunc: method is nil but FS.ReadFile was just called") - } - callInfo := struct { - Path string - }{ - Path: path, - } - mock.lockReadFile.Lock() - mock.calls.ReadFile = append(mock.calls.ReadFile, callInfo) - mock.lockReadFile.Unlock() - return mock.ReadFileFunc(path) -} - -// ReadFileCalls gets all the calls that were made to ReadFile. -// Check the length with: -// -// len(mockedFS.ReadFileCalls()) -func (mock *FSMock) ReadFileCalls() []struct { - Path string -} { - var calls []struct { - Path string - } - mock.lockReadFile.RLock() - calls = mock.calls.ReadFile - mock.lockReadFile.RUnlock() - return calls -} - -// Realpath calls RealpathFunc. -func (mock *FSMock) Realpath(path string) string { - if mock.RealpathFunc == nil { - panic("FSMock.RealpathFunc: method is nil but FS.Realpath was just called") - } - callInfo := struct { - Path string - }{ - Path: path, - } - mock.lockRealpath.Lock() - mock.calls.Realpath = append(mock.calls.Realpath, callInfo) - mock.lockRealpath.Unlock() - return mock.RealpathFunc(path) -} - -// RealpathCalls gets all the calls that were made to Realpath. -// Check the length with: -// -// len(mockedFS.RealpathCalls()) -func (mock *FSMock) RealpathCalls() []struct { - Path string -} { - var calls []struct { - Path string - } - mock.lockRealpath.RLock() - calls = mock.calls.Realpath - mock.lockRealpath.RUnlock() - return calls -} - -// Remove calls RemoveFunc. -func (mock *FSMock) Remove(path string) error { - if mock.RemoveFunc == nil { - panic("FSMock.RemoveFunc: method is nil but FS.Remove was just called") - } - callInfo := struct { - Path string - }{ - Path: path, - } - mock.lockRemove.Lock() - mock.calls.Remove = append(mock.calls.Remove, callInfo) - mock.lockRemove.Unlock() - return mock.RemoveFunc(path) -} - -// RemoveCalls gets all the calls that were made to Remove. -// Check the length with: -// -// len(mockedFS.RemoveCalls()) -func (mock *FSMock) RemoveCalls() []struct { - Path string -} { - var calls []struct { - Path string - } - mock.lockRemove.RLock() - calls = mock.calls.Remove - mock.lockRemove.RUnlock() - return calls -} - -// Stat calls StatFunc. -func (mock *FSMock) Stat(path string) vfs.FileInfo { - if mock.StatFunc == nil { - panic("FSMock.StatFunc: method is nil but FS.Stat was just called") - } - callInfo := struct { - Path string - }{ - Path: path, - } - mock.lockStat.Lock() - mock.calls.Stat = append(mock.calls.Stat, callInfo) - mock.lockStat.Unlock() - return mock.StatFunc(path) -} - -// StatCalls gets all the calls that were made to Stat. -// Check the length with: -// -// len(mockedFS.StatCalls()) -func (mock *FSMock) StatCalls() []struct { - Path string -} { - var calls []struct { - Path string - } - mock.lockStat.RLock() - calls = mock.calls.Stat - mock.lockStat.RUnlock() - return calls -} - -// UseCaseSensitiveFileNames calls UseCaseSensitiveFileNamesFunc. -func (mock *FSMock) UseCaseSensitiveFileNames() bool { - if mock.UseCaseSensitiveFileNamesFunc == nil { - panic("FSMock.UseCaseSensitiveFileNamesFunc: method is nil but FS.UseCaseSensitiveFileNames was just called") - } - callInfo := struct{}{} - mock.lockUseCaseSensitiveFileNames.Lock() - mock.calls.UseCaseSensitiveFileNames = append(mock.calls.UseCaseSensitiveFileNames, callInfo) - mock.lockUseCaseSensitiveFileNames.Unlock() - return mock.UseCaseSensitiveFileNamesFunc() -} - -// UseCaseSensitiveFileNamesCalls gets all the calls that were made to UseCaseSensitiveFileNames. -// Check the length with: -// -// len(mockedFS.UseCaseSensitiveFileNamesCalls()) -func (mock *FSMock) UseCaseSensitiveFileNamesCalls() []struct{} { - var calls []struct{} - mock.lockUseCaseSensitiveFileNames.RLock() - calls = mock.calls.UseCaseSensitiveFileNames - mock.lockUseCaseSensitiveFileNames.RUnlock() - return calls -} - -// WalkDir calls WalkDirFunc. -func (mock *FSMock) WalkDir(root string, walkFn vfs.WalkDirFunc) error { - if mock.WalkDirFunc == nil { - panic("FSMock.WalkDirFunc: method is nil but FS.WalkDir was just called") - } - callInfo := struct { - Root string - WalkFn vfs.WalkDirFunc - }{ - Root: root, - WalkFn: walkFn, - } - mock.lockWalkDir.Lock() - mock.calls.WalkDir = append(mock.calls.WalkDir, callInfo) - mock.lockWalkDir.Unlock() - return mock.WalkDirFunc(root, walkFn) -} - -// WalkDirCalls gets all the calls that were made to WalkDir. -// Check the length with: -// -// len(mockedFS.WalkDirCalls()) -func (mock *FSMock) WalkDirCalls() []struct { - Root string - WalkFn vfs.WalkDirFunc -} { - var calls []struct { - Root string - WalkFn vfs.WalkDirFunc - } - mock.lockWalkDir.RLock() - calls = mock.calls.WalkDir - mock.lockWalkDir.RUnlock() - return calls -} - -// WriteFile calls WriteFileFunc. -func (mock *FSMock) WriteFile(path string, data string, writeByteOrderMark bool) error { - if mock.WriteFileFunc == nil { - panic("FSMock.WriteFileFunc: method is nil but FS.WriteFile was just called") - } - callInfo := struct { - Path string - Data string - WriteByteOrderMark bool - }{ - Path: path, - Data: data, - WriteByteOrderMark: writeByteOrderMark, - } - mock.lockWriteFile.Lock() - mock.calls.WriteFile = append(mock.calls.WriteFile, callInfo) - mock.lockWriteFile.Unlock() - return mock.WriteFileFunc(path, data, writeByteOrderMark) -} - -// WriteFileCalls gets all the calls that were made to WriteFile. -// Check the length with: -// -// len(mockedFS.WriteFileCalls()) -func (mock *FSMock) WriteFileCalls() []struct { - Path string - Data string - WriteByteOrderMark bool -} { - var calls []struct { - Path string - Data string - WriteByteOrderMark bool - } - mock.lockWriteFile.RLock() - calls = mock.calls.WriteFile - mock.lockWriteFile.RUnlock() - return calls -} diff --git a/kitcom/internal/tsgo/vfs/vfsmock/wrapper.go b/kitcom/internal/tsgo/vfs/vfsmock/wrapper.go deleted file mode 100644 index 2c85a09..0000000 --- a/kitcom/internal/tsgo/vfs/vfsmock/wrapper.go +++ /dev/null @@ -1,20 +0,0 @@ -package vfsmock - -import "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" - -// Wrap wraps a vfs.FS and returns a FSMock which calls it. -func Wrap(fs vfs.FS) *FSMock { - return &FSMock{ - DirectoryExistsFunc: fs.DirectoryExists, - FileExistsFunc: fs.FileExists, - GetAccessibleEntriesFunc: fs.GetAccessibleEntries, - ReadFileFunc: fs.ReadFile, - RealpathFunc: fs.Realpath, - RemoveFunc: fs.Remove, - ChtimesFunc: fs.Chtimes, - StatFunc: fs.Stat, - UseCaseSensitiveFileNamesFunc: fs.UseCaseSensitiveFileNames, - WalkDirFunc: fs.WalkDir, - WriteFileFunc: fs.WriteFile, - } -} diff --git a/kitcom/internal/tsgo/vfs/vfstest/vfstest.go b/kitcom/internal/tsgo/vfs/vfstest/vfstest.go deleted file mode 100644 index 1f4feba..0000000 --- a/kitcom/internal/tsgo/vfs/vfstest/vfstest.go +++ /dev/null @@ -1,614 +0,0 @@ -package vfstest - -import ( - "errors" - "fmt" - "io/fs" - "iter" - "maps" - "path" - "slices" - "strings" - "sync" - "testing/fstest" - "time" - - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" - "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs/iovfs" -) - -type MapFS struct { - // mu protects m. - // A single mutex is sufficient as we only use fstest.Map's Open method. - mu sync.RWMutex - - // keys in m are canonicalPaths - m fstest.MapFS - - useCaseSensitiveFileNames bool - - symlinks map[canonicalPath]canonicalPath - - clock Clock -} - -type Clock interface { - Now() time.Time - SinceStart() time.Duration -} - -type clockImpl struct { - start time.Time -} - -func (c *clockImpl) Now() time.Time { - return time.Now() -} - -func (c *clockImpl) SinceStart() time.Duration { - return time.Since(c.start) -} - -var ( - _ iovfs.RealpathFS = (*MapFS)(nil) - _ iovfs.WritableFS = (*MapFS)(nil) -) - -type sys struct { - original any - realpath string -} - -// FromMap creates a new [vfs.FS] from a map of paths to file contents. -// Those file contents may be strings, byte slices, or [fstest.MapFile]s. -// -// The paths must be normalized absolute paths according to the tspath package, -// without trailing directory separators. -// The paths must be all POSIX-style or all Windows-style, but not both. -func FromMap[File any](m map[string]File, useCaseSensitiveFileNames bool) vfs.FS { - return FromMapWithClock(m, useCaseSensitiveFileNames, &clockImpl{start: time.Now()}) -} - -// FromMapWithClock creates a new [vfs.FS] from a map of paths to file contents. -// Those file contents may be strings, byte slices, or [fstest.MapFile]s. -// -// The paths must be normalized absolute paths according to the tspath package, -// without trailing directory separators. -// The paths must be all POSIX-style or all Windows-style, but not both. -func FromMapWithClock[File any](m map[string]File, useCaseSensitiveFileNames bool, clock Clock) vfs.FS { - posix := false - windows := false - - checkPath := func(p string) { - if !tspath.IsRootedDiskPath(p) { - panic(fmt.Sprintf("non-rooted path %q", p)) - } - - if normal := tspath.RemoveTrailingDirectorySeparator(tspath.NormalizePath(p)); normal != p { - panic(fmt.Sprintf("non-normalized path %q", p)) - } - - if strings.HasPrefix(p, "/") { - posix = true - } else { - windows = true - } - } - - mfs := make(fstest.MapFS, len(m)) - // Sorted creation to ensure times are always guaranteed to be in order. - keys := slices.Collect(maps.Keys(m)) - slices.SortFunc(keys, comparePathsByParts) - for _, p := range keys { - f := m[p] - checkPath(p) - - var file *fstest.MapFile - switch f := any(f).(type) { - case string: - file = &fstest.MapFile{Data: []byte(f), ModTime: clock.Now()} - case []byte: - file = &fstest.MapFile{Data: f, ModTime: clock.Now()} - case *fstest.MapFile: - fCopy := *f - fCopy.ModTime = clock.Now() - file = &fCopy - default: - panic(fmt.Sprintf("invalid file type %T", f)) - } - - if file.Mode&fs.ModeSymlink != 0 { - target := string(file.Data) - checkPath(target) - - target, _ = strings.CutPrefix(target, "/") - fileCopy := *file - fileCopy.Data = []byte(target) - file = &fileCopy - } - - p, _ = strings.CutPrefix(p, "/") - mfs[p] = file - } - - if posix && windows { - panic("mixed posix and windows paths") - } - - return iovfs.From(convertMapFS(mfs, useCaseSensitiveFileNames, clock), useCaseSensitiveFileNames) -} - -func convertMapFS(input fstest.MapFS, useCaseSensitiveFileNames bool, clock Clock) *MapFS { - if clock == nil { - clock = &clockImpl{start: time.Now()} - } - m := &MapFS{ - m: make(fstest.MapFS, len(input)), - useCaseSensitiveFileNames: useCaseSensitiveFileNames, - clock: clock, - } - - // Verify that the input is well-formed. - canonicalPaths := make(map[canonicalPath]string, len(input)) - for path := range input { - canonical := m.getCanonicalPath(path) - if other, ok := canonicalPaths[canonical]; ok { - // Ensure consistent panic messages - path, other = min(path, other), max(path, other) - panic(fmt.Sprintf("duplicate path: %q and %q have the same canonical path", path, other)) - } - canonicalPaths[canonical] = path - } - - // Sort the input by depth and path so we ensure parent dirs are created - // before their children, if explicitly specified by the input. - inputKeys := slices.Collect(maps.Keys(input)) - slices.SortFunc(inputKeys, comparePathsByParts) - - for _, p := range inputKeys { - file := input[p] - - // Create all missing intermediate directories so we can attach the realpath to each of them. - // fstest.MapFS doesn't require this as it synthesizes directories on the fly, but it's a lot - // harder to reapply a realpath onto those when we're deep in some FileInfo method. - if dir := dirName(p); dir != "" { - if err := m.mkdirAll(dir, 0o777); err != nil { - panic(fmt.Sprintf("failed to create intermediate directories for %q: %v", p, err)) - } - } - m.setEntry(p, m.getCanonicalPath(p), *file) - } - - return m -} - -func comparePathsByParts(a, b string) int { - for { - aStart, aEnd, aOk := strings.Cut(a, "/") - bStart, bEnd, bOk := strings.Cut(b, "/") - - if !aOk || !bOk { - return strings.Compare(a, b) - } - - if r := strings.Compare(aStart, bStart); r != 0 { - return r - } - - a, b = aEnd, bEnd - } -} - -type canonicalPath string - -func (m *MapFS) getCanonicalPath(p string) canonicalPath { - return canonicalPath(tspath.GetCanonicalFileName(p, m.useCaseSensitiveFileNames)) -} - -func (m *MapFS) open(p canonicalPath) (fs.File, error) { - return m.m.Open(string(p)) -} - -func (m *MapFS) remove(path string) error { - canonical := m.getCanonicalPath(path) - canonicalString := string(canonical) - fileInfo := m.m[canonicalString] - if fileInfo == nil { - // file does not exist - return nil - } - delete(m.m, canonicalString) - delete(m.symlinks, canonical) - - if fileInfo.Mode.IsDir() { - canonicalString += "/" - for path := range m.m { - if strings.HasPrefix(path, canonicalString) { - delete(m.m, path) - delete(m.symlinks, canonicalPath(path)) - } - } - } - return nil -} - -func Symlink(target string) *fstest.MapFile { - return &fstest.MapFile{ - Data: []byte(target), - Mode: fs.ModeSymlink, - } -} - -func (m *MapFS) getFollowingSymlinks(p canonicalPath) (*fstest.MapFile, canonicalPath, error) { - return m.getFollowingSymlinksWorker(p, "", "") -} - -type brokenSymlinkError struct { - from, to canonicalPath -} - -func (e *brokenSymlinkError) Error() string { - return fmt.Sprintf("broken symlink %q -> %q", e.from, e.to) -} - -func (m *MapFS) getFollowingSymlinksWorker(p canonicalPath, symlinkFrom, symlinkTo canonicalPath) (*fstest.MapFile, canonicalPath, error) { - if file, ok := m.m[string(p)]; ok && file.Mode&fs.ModeSymlink == 0 { - return file, p, nil - } - - if target, ok := m.symlinks[p]; ok { - return m.getFollowingSymlinksWorker(target, p, target) - } - - // This could be a path underneath a symlinked directory. - for other, target := range m.symlinks { - if len(other) < len(p) && other == p[:len(other)] && p[len(other)] == '/' { - return m.getFollowingSymlinksWorker(target+p[len(other):], other, target) - } - } - - err := fs.ErrNotExist - if symlinkFrom != "" { - err = &brokenSymlinkError{symlinkFrom, symlinkTo} - } - return nil, p, err -} - -func (m *MapFS) set(p canonicalPath, file *fstest.MapFile) { - m.m[string(p)] = file -} - -func (m *MapFS) setEntry(realpath string, canonical canonicalPath, file fstest.MapFile) { - if realpath == "" || canonical == "" { - panic("empty path") - } - - file.Sys = &sys{ - original: file.Sys, - realpath: realpath, - } - m.set(canonical, &file) - - if file.Mode&fs.ModeSymlink != 0 { - if m.symlinks == nil { - m.symlinks = make(map[canonicalPath]canonicalPath) - } - m.symlinks[canonical] = m.getCanonicalPath(string(file.Data)) - } -} - -func splitPath(s string, offset int) (before, after string) { - idx := strings.IndexByte(s[offset:], '/') - if idx < 0 { - return s, "" - } - return s[:idx+offset], s[idx+1+offset:] -} - -func dirName(p string) string { - dir, _ := path.Split(p) - return strings.TrimSuffix(dir, "/") -} - -func baseName(p string) string { - _, file := path.Split(p) - return file -} - -func (m *MapFS) mkdirAll(p string, perm fs.FileMode) error { - if p == "" { - panic("empty path") - } - - // Fast path; already exists. - if other, _, err := m.getFollowingSymlinks(m.getCanonicalPath(p)); err == nil { - if !other.Mode.IsDir() { - return fmt.Errorf("mkdir %q: path exists but is not a directory", p) - } - return nil - } - - var toCreate []string - offset := 0 - for { - dir, rest := splitPath(p, offset) - canonical := m.getCanonicalPath(dir) - other, otherPath, err := m.getFollowingSymlinks(canonical) - if err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return err - } - toCreate = append(toCreate, dir) - } else { - if !other.Mode.IsDir() { - return fmt.Errorf("mkdir %q: path exists but is not a directory", otherPath) - } - if canonical != otherPath { - // We have a symlinked parent, reset and start again. - p = other.Sys.(*sys).realpath + "/" + rest - toCreate = toCreate[:0] - offset = 0 - continue - } - } - if rest == "" { - break - } - offset = len(dir) + 1 - } - - for _, dir := range toCreate { - m.setEntry(dir, m.getCanonicalPath(dir), fstest.MapFile{ - Mode: fs.ModeDir | perm&^umask, - ModTime: m.clock.Now(), - }) - } - - return nil -} - -type fileInfo struct { - fs.FileInfo - sys any - realpath string -} - -func (fi *fileInfo) Name() string { - return baseName(fi.realpath) -} - -func (fi *fileInfo) Sys() any { - return fi.sys -} - -type file struct { - fs.File - fileInfo *fileInfo -} - -func (f *file) Stat() (fs.FileInfo, error) { - return f.fileInfo, nil -} - -type readDirFile struct { - fs.ReadDirFile - fileInfo *fileInfo -} - -func (f *readDirFile) Stat() (fs.FileInfo, error) { - return f.fileInfo, nil -} - -func (f *readDirFile) ReadDir(n int) ([]fs.DirEntry, error) { - list, err := f.ReadDirFile.ReadDir(n) - if err != nil { - return nil, err - } - - entries := make([]fs.DirEntry, len(list)) - for i, entry := range list { - info := must(entry.Info()) - newInfo, ok := convertInfo(info) - if !ok { - panic(fmt.Sprintf("unexpected synthesized dir: %q", info.Name())) - } - entries[i] = fs.FileInfoToDirEntry(newInfo) - } - - return entries, nil -} - -func (m *MapFS) Open(name string) (fs.File, error) { - m.mu.RLock() - defer m.mu.RUnlock() - - _, cp, _ := m.getFollowingSymlinks(m.getCanonicalPath(name)) - f, err := m.open(cp) - if err != nil { - return nil, err - } - - info := must(f.Stat()) - - newInfo, ok := convertInfo(info) - if !ok { - // This is a synthesized dir. - if name != "." { - panic(fmt.Sprintf("unexpected synthesized dir: %q", name)) - } - - return &readDirFile{ - ReadDirFile: f.(fs.ReadDirFile), - fileInfo: &fileInfo{ - FileInfo: info, - sys: info.Sys(), - realpath: ".", - }, - }, nil - } - - if f, ok := f.(fs.ReadDirFile); ok { - return &readDirFile{ - ReadDirFile: f, - fileInfo: newInfo, - }, nil - } - - return &file{ - File: f, - fileInfo: newInfo, - }, nil -} - -func (m *MapFS) Realpath(name string) (string, error) { - m.mu.RLock() - defer m.mu.RUnlock() - - file, _, err := m.getFollowingSymlinks(m.getCanonicalPath(name)) - if err != nil { - return "", err - } - return file.Sys.(*sys).realpath, nil -} - -func convertInfo(info fs.FileInfo) (*fileInfo, bool) { - sys, ok := info.Sys().(*sys) - if !ok { - return nil, false - } - return &fileInfo{ - FileInfo: info, - sys: sys.original, - realpath: sys.realpath, - }, true -} - -const umask = 0o022 - -func (m *MapFS) MkdirAll(path string, perm fs.FileMode) error { - m.mu.Lock() - defer m.mu.Unlock() - - return m.mkdirAll(path, perm) -} - -func (m *MapFS) WriteFile(path string, data []byte, perm fs.FileMode) error { - m.mu.Lock() - defer m.mu.Unlock() - - if parent := dirName(path); parent != "" { - canonical := m.getCanonicalPath(parent) - parentFile, _, err := m.getFollowingSymlinks(canonical) - if err != nil { - return fmt.Errorf("write %q: %w", path, err) - } - if !parentFile.Mode.IsDir() { - return fmt.Errorf("write %q: parent path exists but is not a directory", path) - } - } - - file, cp, err := m.getFollowingSymlinks(m.getCanonicalPath(path)) - if err != nil { - var brokenSymlinkError *brokenSymlinkError - if !errors.Is(err, fs.ErrNotExist) && !errors.As(err, &brokenSymlinkError) { - // No other errors are possible. - panic(err) - } - } else { - if !file.Mode.IsRegular() { - return fmt.Errorf("write %q: path exists but is not a regular file", path) - } - } - - m.setEntry(path, cp, fstest.MapFile{ - Data: data, - ModTime: m.clock.Now(), - Mode: perm &^ umask, - }) - - return nil -} - -func (m *MapFS) Remove(path string) error { - m.mu.Lock() - defer m.mu.Unlock() - - return m.remove(path) -} - -func (m *MapFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { - m.mu.Lock() - defer m.mu.Unlock() - canonical := m.getCanonicalPath(path) - canonicalString := string(canonical) - fileInfo := m.m[canonicalString] - if fileInfo == nil { - // file does not exist - return fs.ErrNotExist - } - fileInfo.ModTime = mTime - return nil -} - -func (m *MapFS) GetTargetOfSymlink(path string) (string, bool) { - path, _ = strings.CutPrefix(path, "/") - m.mu.RLock() - defer m.mu.RUnlock() - canonical := m.getCanonicalPath(path) - canonicalString := string(canonical) - if fileInfo, ok := m.m[canonicalString]; ok { - if fileInfo.Mode&fs.ModeSymlink != 0 { - return "/" + string(fileInfo.Data), true - } - } - return "", false -} - -func (m *MapFS) GetModTime(path string) time.Time { - path, _ = strings.CutPrefix(path, "/") - m.mu.RLock() - defer m.mu.RUnlock() - canonical := m.getCanonicalPath(path) - canonicalString := string(canonical) - if fileInfo, ok := m.m[canonicalString]; ok { - return fileInfo.ModTime - } - return time.Time{} -} - -func (m *MapFS) Entries() iter.Seq2[string, *fstest.MapFile] { - return func(yield func(string, *fstest.MapFile) bool) { - m.mu.RLock() - defer m.mu.RUnlock() - inputKeys := slices.Collect(maps.Keys(m.m)) - slices.SortFunc(inputKeys, comparePathsByParts) - - for _, p := range inputKeys { - file := m.m[p] - path := file.Sys.(*sys).realpath - if !tspath.PathIsAbsolute(path) { - path = "/" + path - } - if !yield(path, file) { - break - } - } - } -} - -func (m *MapFS) GetFileInfo(path string) *fstest.MapFile { - path, _ = strings.CutPrefix(path, "/") - m.mu.RLock() - defer m.mu.RUnlock() - canonical := m.getCanonicalPath(path) - canonicalString := string(canonical) - return m.m[canonicalString] -} - -func must[T any](v T, err error) T { - if err != nil { - panic(err) - } - return v -}