Repository: stoplightio/spectral (ts)

Issue:
spectral custom rule attribute "field" not reset when invoking multiple functions on single rule. 
**Describe the bug**
 `field` attribute is not cleared when multiple functions used in same rule. Same behavior on both CLI and JS invocation. 

The VS Code extension seems to not have this same defect.

**To Reproduce**

* Given this OpenAPI/AsyncAPI document '...'
```
Example1:
  title: Example1
  description: Example1
  type: string
  enum:
    - enum11
    - enum12
  example: enum12
Example2:
  title: Example2
  description: Example2
  type: string
  enum:
    - enum21
    - enum22
  example: enum22
```

* And this spectral rule:
```  
functions: [Rules]
rules:
  bug-fields:
    given: "$.*"
    then:
      - function: Rules
        functionOptions:
          rules:
            - Print1
      - field: "title"
        function: Rules
        functionOptions:
          rules:
            - Print2
 ```
(Where the function "Rules" invokes the sub-function/rule specified in "rules")

* With JS those custom functions defined simply as:
```
function Print1(results, input, path, rule) {
    console.log("Print1", path)
}
function Print2(results, input, path, rule) {
    console.log("Print2", path)
}
```

* Invoked both in CLI and JS, result via console is:

`npx spectral lint --ruleset .spectral.yaml .\placeholder.yaml --verbose`


```
Print1 [ 'Example1' ]
Print2 [ 'Example1', 'title' ]
Print1 [ 'Example2' ]
Print2 [ 'Example2', 'title' ]
```

However, the issue occurs if I include the "field" attribute to the first entry, instead of the second, like so:
```  
  bug-fields:
    given: "$.*"
    then:
      - field: "title"
        function: Rules
        functionOptions:
          rules:
            - Print1
      - function: Rules
        functionOptions:
          rules:
            - Print2
```

**Expected behavior**

`field` attribute is only applied to the function where it is defined. 

In the VS Code Spectral Extension, this is also what happens. 

```
Print1 [ 'Example1', 'title' ]
Print2 [ 'Example1' ]
Print1 [ 'Example2', 'title' ]
Print2 [ 'Example2' ]
```

**Observed output**

`field` attribute is not cleared when invoking the Print2 function, resulting in the execution order of these separate functions influencing each other. Which I assume is not intended behavior. 

```
Print1 [ 'Example1', 'title' ]
Print2 [ 'Example1', 'title' ]
Print1 [ 'Example2', 'title' ]
Print2 [ 'Example2', 'title' ]
```


**Environment (remove any that are not applicable):**
 - Library version: 6.6.0
 - OS: Windows 11
 - VS Code Spectral Extension: Spectral  v1.1.0 


Let me know if I need to provide more information to clarify!


Pull request:
fix(core): reset path in fn context
Fixes #2377 

**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No



Patch:
diff --git a/packages/core/src/runner/lintNode.ts b/packages/core/src/runner/lintNode.ts
index 5e232cff..2dcf931c 100644
--- a/packages/core/src/runner/lintNode.ts
+++ b/packages/core/src/runner/lintNode.ts
@@ -24,6 +24,8 @@ export const lintNode = (context: IRunnerInternalContext, node: IGivenNode, rule
     for (const target of targets) {
       if (target.path.length > 0) {
         fnContext.path = [...givenPath, ...target.path];
+      } else {
+        fnContext.path = givenPath;
       }
 
       let targetResults;


Test patch:
diff --git a/packages/core/src/__tests__/linter.test.ts b/packages/core/src/__tests__/linter.test.ts
index f9193421..21f592b6 100644
--- a/packages/core/src/__tests__/linter.test.ts
+++ b/packages/core/src/__tests__/linter.test.ts
@@ -1682,4 +1682,71 @@ responses:: !!foo
       }),
     ]);
   });
+
+  test.concurrent('should reset path provided in fn context', async () => {
+    const spectral = new Spectral();
+    const fn = jest.fn();
+
+    spectral.setRuleset({
+      rules: {
+        'valid-info': {
+          given: '$.info',
+          then: [
+            {
+              field: 'title',
+              function: truthy,
+            },
+            {
+              function: fn,
+            },
+            {
+              field: 'description',
+              function: truthy,
+            },
+            {
+              function: fn,
+            },
+          ],
+        },
+      },
+    });
+
+    const documentUri = path.join(__dirname, './__fixtures__/test.json');
+    const document = new Document(
+      JSON.stringify({
+        info: {
+          title: 'test',
+          description: 'some description',
+        },
+      }),
+      Parsers.Json,
+      documentUri,
+    );
+
+    await expect(spectral.run(document)).resolves.toEqual([]);
+
+    expect(fn).nthCalledWith(
+      1,
+      {
+        title: 'test',
+        description: 'some description',
+      },
+      null,
+      expect.objectContaining({
+        path: ['info'],
+      }),
+    );
+
+    expect(fn).nthCalledWith(
+      2,
+      {
+        title: 'test',
+        description: 'some description',
+      },
+      null,
+      expect.objectContaining({
+        path: ['info'],
+      }),
+    );
+  });
 });