diff --git a/tests/codeblocks/javascript/ReactContextPropagation-test.js b/tests/codeblocks/javascript/ReactContextPropagation-test.js new file mode 100644 index 00000000..37c47717 --- /dev/null +++ b/tests/codeblocks/javascript/ReactContextPropagation-test.js @@ -0,0 +1,926 @@ +let React; +let ReactNoop; +let Scheduler; +let act; +let useState; +let useContext; +let Suspense; +let SuspenseList; +let getCacheForType; +let caches; +let seededCache; +let assertLog; + +describe('ReactLazyContextPropagation', () => { + beforeEach(() => { + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + act = require('internal-test-utils').act; + useState = React.useState; + useContext = React.useContext; + Suspense = React.Suspense; + if (gate(flags => flags.enableSuspenseList)) { + SuspenseList = React.unstable_SuspenseList; + } + + const InternalTestUtils = require('internal-test-utils'); + assertLog = InternalTestUtils.assertLog; + + getCacheForType = React.unstable_getCacheForType; + + caches = []; + seededCache = null; + }); + + // NOTE: These tests are not specific to the lazy propagation (as opposed to + // eager propagation). The behavior should be the same in both + // implementations. These are tests that are more relevant to the lazy + // propagation implementation, though. + + function createTextCache() { + if (seededCache !== null) { + // Trick to seed a cache before it exists. + // TODO: Need a built-in API to seed data before the initial render (i.e. + // not a refresh because nothing has mounted yet). + const cache = seededCache; + seededCache = null; + return cache; + } + + const data = new Map(); + const version = caches.length + 1; + const cache = { + version, + data, + resolve(text) { + const record = data.get(text); + if (record === undefined) { + const newRecord = { + status: 'resolved', + value: text, + }; + data.set(text, newRecord); + } else if (record.status === 'pending') { + const thenable = record.value; + record.status = 'resolved'; + record.value = text; + thenable.pings.forEach(t => t()); + } + }, + reject(text, error) { + const record = data.get(text); + if (record === undefined) { + const newRecord = { + status: 'rejected', + value: error, + }; + data.set(text, newRecord); + } else if (record.status === 'pending') { + const thenable = record.value; + record.status = 'rejected'; + record.value = error; + thenable.pings.forEach(t => t()); + } + }, + }; + caches.push(cache); + return cache; + } + + function readText(text) { + const textCache = getCacheForType(createTextCache); + const record = textCache.data.get(text); + if (record !== undefined) { + switch (record.status) { + case 'pending': + Scheduler.log(`Suspend! [${text}]`); + throw record.value; + case 'rejected': + Scheduler.log(`Error! [${text}]`); + throw record.value; + case 'resolved': + return textCache.version; + } + } else { + Scheduler.log(`Suspend! [${text}]`); + + const thenable = { + pings: [], + then(resolve) { + if (newRecord.status === 'pending') { + thenable.pings.push(resolve); + } else { + Promise.resolve().then(() => resolve(newRecord.value)); + } + }, + }; + + const newRecord = { + status: 'pending', + value: thenable, + }; + textCache.data.set(text, newRecord); + + throw thenable; + } + } + + function Text({text}) { + Scheduler.log(text); + return text; + } + + // function AsyncText({text, showVersion}) { + // const version = readText(text); + // const fullText = showVersion ? `${text} [v${version}]` : text; + // Scheduler.log(fullText); + // return text; + // } + + function seedNextTextCache(text) { + if (seededCache === null) { + seededCache = createTextCache(); + } + seededCache.resolve(text); + } + + function resolveMostRecentTextCache(text) { + if (caches.length === 0) { + throw Error('Cache does not exist.'); + } else { + // Resolve the most recently created cache. An older cache can by + // resolved with `caches[index].resolve(text)`. + caches[caches.length - 1].resolve(text); + } + } + + const resolveText = resolveMostRecentTextCache; + + // function rejectMostRecentTextCache(text, error) { + // if (caches.length === 0) { + // throw Error('Cache does not exist.'); + // } else { + // // Resolve the most recently created cache. An older cache can by + // // resolved with `caches[index].reject(text, error)`. + // caches[caches.length - 1].reject(text, error); + // } + // } + + test( + 'context change should prevent bailout of memoized component (useMemo -> ' + + 'no intermediate fiber)', + async () => { + const root = ReactNoop.createRoot(); + + const Context = React.createContext(0); + + let setValue; + function App() { + const [value, _setValue] = useState(0); + setValue = _setValue; + + // NOTE: It's an important part of this test that we're memoizing the + // props of the Consumer component, as opposed to wrapping in an + // additional memoized fiber, because the implementation propagates + // context changes whenever a fiber bails out. + const consumer = React.useMemo(() => , []); + + return {consumer}; + } + + function Consumer() { + const value = useContext(Context); + // Even though Consumer is memoized, Consumer should re-render + // DeepChild whenever the context value changes. Otherwise DeepChild + // won't receive the new value. + return ; + } + + function DeepChild({value}) { + return ; + } + + await act(() => { + root.render(); + }); + assertLog([0]); + expect(root).toMatchRenderedOutput('0'); + + await act(() => { + setValue(1); + }); + assertLog([1]); + expect(root).toMatchRenderedOutput('1'); + }, + ); + + test('context change should prevent bailout of memoized component (memo HOC)', async () => { + const root = ReactNoop.createRoot(); + + const Context = React.createContext(0); + + let setValue; + function App() { + const [value, _setValue] = useState(0); + setValue = _setValue; + return ( + + + + ); + } + + const Consumer = React.memo(() => { + const value = useContext(Context); + // Even though Consumer is memoized, Consumer should re-render + // DeepChild whenever the context value changes. Otherwise DeepChild + // won't receive the new value. + return ; + }); + + function DeepChild({value}) { + return ; + } + + await act(() => { + root.render(); + }); + assertLog([0]); + expect(root).toMatchRenderedOutput('0'); + + await act(() => { + setValue(1); + }); + assertLog([1]); + expect(root).toMatchRenderedOutput('1'); + }); + + test('context change should prevent bailout of memoized component (PureComponent)', async () => { + const root = ReactNoop.createRoot(); + + const Context = React.createContext(0); + + let setValue; + function App() { + const [value, _setValue] = useState(0); + setValue = _setValue; + return ( + + + + ); + } + + class Consumer extends React.PureComponent { + static contextType = Context; + render() { + // Even though Consumer is memoized, Consumer should re-render + // DeepChild whenever the context value changes. Otherwise DeepChild + // won't receive the new value. + return ; + } + } + + function DeepChild({value}) { + return ; + } + + await act(() => { + root.render(); + }); + assertLog([0]); + expect(root).toMatchRenderedOutput('0'); + + await act(() => { + setValue(1); + }); + assertLog([1]); + expect(root).toMatchRenderedOutput('1'); + }); + + test("context consumer bails out if context hasn't changed", async () => { + const root = ReactNoop.createRoot(); + + const Context = React.createContext(0); + + function App() { + return ( + + + + ); + } + + let setOtherValue; + const Consumer = React.memo(() => { + const value = useContext(Context); + + const [, _setOtherValue] = useState(0); + setOtherValue = _setOtherValue; + + Scheduler.log('Consumer'); + + return ; + }); + + await act(() => { + root.render(); + }); + assertLog(['Consumer', 0]); + expect(root).toMatchRenderedOutput('0'); + + await act(() => { + // Intentionally calling setState to some other arbitrary value before + // setting it back to the current one. That way an update is scheduled, + // but we'll bail out during render when nothing has changed. + setOtherValue(1); + setOtherValue(0); + }); + // NOTE: If this didn't yield anything, that indicates that we never visited + // the consumer during the render phase, which probably means the eager + // bailout mechanism kicked in. Because we're testing the _lazy_ bailout + // mechanism, update this test to foil the _eager_ bailout, somehow. Perhaps + // by switching to useReducer. + assertLog(['Consumer']); + expect(root).toMatchRenderedOutput('0'); + }); + + // @gate enableLegacyCache + test('context is propagated across retries', async () => { + const root = ReactNoop.createRoot(); + + const Context = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + return ( + + }> + + + + + ); + } + + function Async() { + const value = useContext(Context); + readText(value); + + // When `readText` suspends, we haven't yet visited Indirection and all + // of its children. They won't get rendered until a later retry. + return ; + } + + const Indirection = React.memo(() => { + // This child must always be consistent with the sibling Text component. + return ; + }); + + function DeepChild() { + const value = useContext(Context); + return ; + } + + await seedNextTextCache('A'); + await act(() => { + root.render(); + }); + assertLog(['A', 'A']); + expect(root).toMatchRenderedOutput('AA'); + + await act(() => { + // Intentionally not wrapping in startTransition, so that the fallback + // the fallback displays despite this being a refresh. + setContext('B'); + }); + assertLog(['Suspend! [B]', 'Loading...', 'B']); + expect(root).toMatchRenderedOutput('Loading...B'); + + await act(async () => { + await resolveText('B'); + }); + assertLog(['B']); + expect(root).toMatchRenderedOutput('BB'); + }); + + // @gate enableLegacyCache + test('multiple contexts are propagated across retries', async () => { + // Same as previous test, but with multiple context providers + const root = ReactNoop.createRoot(); + + const Context1 = React.createContext('A'); + const Context2 = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + return ( + + + }> + + + + + + ); + } + + function Async() { + const value = useContext(Context1); + readText(value); + + // When `readText` suspends, we haven't yet visited Indirection and all + // of its children. They won't get rendered until a later retry. + return ( + <> + + + + ); + } + + const Indirection1 = React.memo(() => { + // This child must always be consistent with the sibling Text component. + return ; + }); + + const Indirection2 = React.memo(() => { + // This child must always be consistent with the sibling Text component. + return ; + }); + + function DeepChild1() { + const value = useContext(Context1); + return ; + } + + function DeepChild2() { + const value = useContext(Context2); + return ; + } + + await seedNextTextCache('A'); + await act(() => { + root.render(); + }); + assertLog(['A', 'A', 'A']); + expect(root).toMatchRenderedOutput('AAA'); + + await act(() => { + // Intentionally not wrapping in startTransition, so that the fallback + // the fallback displays despite this being a refresh. + setContext('B'); + }); + assertLog(['Suspend! [B]', 'Loading...', 'B']); + expect(root).toMatchRenderedOutput('Loading...B'); + + await act(async () => { + await resolveText('B'); + }); + assertLog(['B', 'B']); + expect(root).toMatchRenderedOutput('BBB'); + }); + + // @gate enableLegacyCache + test('context is propagated across retries (legacy)', async () => { + const root = ReactNoop.createLegacyRoot(); + + const Context = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + return ( + + }> + + + + + ); + } + + function Async() { + const value = useContext(Context); + readText(value); + + // When `readText` suspends, we haven't yet visited Indirection and all + // of its children. They won't get rendered until a later retry. + return ; + } + + const Indirection = React.memo(() => { + // This child must always be consistent with the sibling Text component. + return ; + }); + + function DeepChild() { + const value = useContext(Context); + return ; + } + + await seedNextTextCache('A'); + await act(() => { + root.render(); + }); + assertLog(['A', 'A']); + expect(root).toMatchRenderedOutput('AA'); + + await act(() => { + // Intentionally not wrapping in startTransition, so that the fallback + // the fallback displays despite this being a refresh. + setContext('B'); + }); + assertLog(['Suspend! [B]', 'Loading...', 'B']); + expect(root).toMatchRenderedOutput('Loading...B'); + + await act(async () => { + await resolveText('B'); + }); + assertLog(['B']); + expect(root).toMatchRenderedOutput('BB'); + }); + + // @gate www + test('context is propagated through offscreen trees', async () => { + const LegacyHidden = React.unstable_LegacyHidden; + + const root = ReactNoop.createRoot(); + + const Context = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + return ( + + + + + + + ); + } + + const Indirection = React.memo(() => { + // This child must always be consistent with the sibling Text component. + return ; + }); + + function DeepChild() { + const value = useContext(Context); + return ; + } + + await seedNextTextCache('A'); + await act(() => { + root.render(); + }); + assertLog(['A', 'A']); + expect(root).toMatchRenderedOutput('AA'); + + await act(() => { + setContext('B'); + }); + assertLog(['B', 'B']); + expect(root).toMatchRenderedOutput('BB'); + }); + + // @gate www + test('multiple contexts are propagated across through offscreen trees', async () => { + // Same as previous test, but with multiple context providers + const LegacyHidden = React.unstable_LegacyHidden; + + const root = ReactNoop.createRoot(); + + const Context1 = React.createContext('A'); + const Context2 = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + return ( + + + + + + + + + + ); + } + + const Indirection1 = React.memo(() => { + // This child must always be consistent with the sibling Text component. + return ; + }); + + const Indirection2 = React.memo(() => { + // This child must always be consistent with the sibling Text component. + return ; + }); + + function DeepChild1() { + const value = useContext(Context1); + return ; + } + + function DeepChild2() { + const value = useContext(Context2); + return ; + } + + await seedNextTextCache('A'); + await act(() => { + root.render(); + }); + assertLog(['A', 'A', 'A']); + expect(root).toMatchRenderedOutput('AAA'); + + await act(() => { + setContext('B'); + }); + assertLog(['B', 'B', 'B']); + expect(root).toMatchRenderedOutput('BBB'); + }); + + // @gate enableSuspenseList + test('contexts are propagated through SuspenseList', async () => { + // This kinda tests an implementation detail. SuspenseList has an early + // bailout that doesn't use `bailoutOnAlreadyFinishedWork`. It probably + // should just use that function, though. + const Context = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + const children = React.useMemo( + () => ( + + + + + ), + [], + ); + return {children}; + } + + function Child() { + const value = useContext(Context); + return ; + } + + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + assertLog(['A', 'A']); + expect(root).toMatchRenderedOutput('AA'); + + await act(() => { + setContext('B'); + }); + assertLog(['B', 'B']); + expect(root).toMatchRenderedOutput('BB'); + }); + + test('nested bailouts', async () => { + // Lazy context propagation will stop propagating when it hits the first + // match. If we bail out again inside that tree, we must resume propagating. + + const Context = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + return ( + + + + ); + } + + const ChildIndirection = React.memo(() => { + return ; + }); + + function Child() { + const value = useContext(Context); + return ( + <> + + + + ); + } + + const DeepChildIndirection = React.memo(() => { + return ; + }); + + function DeepChild() { + const value = useContext(Context); + return ; + } + + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + assertLog(['A', 'A']); + expect(root).toMatchRenderedOutput('AA'); + + await act(() => { + setContext('B'); + }); + assertLog(['B', 'B']); + expect(root).toMatchRenderedOutput('BB'); + }); + + // @gate enableLegacyCache + test('nested bailouts across retries', async () => { + // Lazy context propagation will stop propagating when it hits the first + // match. If we bail out again inside that tree, we must resume propagating. + + const Context = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + return ( + + }> + + + + ); + } + + function Async({value}) { + // When this suspends, we won't be able to visit its children during the + // current render. So we must take extra care to propagate the context + // change in such a way that they're aren't lost when we retry in a + // later render. + readText(value); + return ; + } + + function Child() { + const value = useContext(Context); + return ( + <> + + + + ); + } + + const DeepChildIndirection = React.memo(() => { + return ; + }); + + function DeepChild() { + const value = useContext(Context); + return ; + } + + const root = ReactNoop.createRoot(); + await seedNextTextCache('A'); + await act(() => { + root.render(); + }); + assertLog(['A', 'A']); + expect(root).toMatchRenderedOutput('AA'); + + await act(() => { + setContext('B'); + }); + assertLog(['Suspend! [B]', 'Loading...']); + expect(root).toMatchRenderedOutput('Loading...'); + + await act(async () => { + await resolveText('B'); + }); + assertLog(['B', 'B']); + expect(root).toMatchRenderedOutput('BB'); + }); + + // @gate www + test('nested bailouts through offscreen trees', async () => { + // Lazy context propagation will stop propagating when it hits the first + // match. If we bail out again inside that tree, we must resume propagating. + + const LegacyHidden = React.unstable_LegacyHidden; + + const Context = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + return ( + + + + + + ); + } + + function Child() { + const value = useContext(Context); + return ( + <> + + + + ); + } + + const DeepChildIndirection = React.memo(() => { + return ; + }); + + function DeepChild() { + const value = useContext(Context); + return ; + } + + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + assertLog(['A', 'A']); + expect(root).toMatchRenderedOutput('AA'); + + await act(() => { + setContext('B'); + }); + assertLog(['B', 'B']); + expect(root).toMatchRenderedOutput('BB'); + }); + + test('finds context consumers in multiple sibling branches', async () => { + // This test confirms that when we find a matching context consumer during + // propagation, we continue propagating to its sibling branches. + + const Context = React.createContext('A'); + + let setContext; + function App() { + const [value, setValue] = useState('A'); + setContext = setValue; + return ( + + + + ); + } + + const Blah = React.memo(() => { + return ( + <> + + + + ); + }); + + const Indirection = React.memo(() => { + return ; + }); + + function Child() { + const value = useContext(Context); + return ; + } + + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + assertLog(['A', 'A']); + expect(root).toMatchRenderedOutput('AA'); + + await act(() => { + setContext('B'); + }); + assertLog(['B', 'B']); + expect(root).toMatchRenderedOutput('BB'); + }); +}); diff --git a/tests/codeblocks/javascript/async_function.js b/tests/codeblocks/javascript/async_function.js new file mode 100644 index 00000000..7bd03a3f --- /dev/null +++ b/tests/codeblocks/javascript/async_function.js @@ -0,0 +1,9 @@ +const asyncFunction = async () => { + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve('Async function resolved'); + }, 1000); + }); +}; + +asyncFunction().then(console.log).catch(console.error); \ No newline at end of file diff --git a/tests/codeblocks/javascript/async_function.txt b/tests/codeblocks/javascript/async_function.txt new file mode 100644 index 00000000..6101601e --- /dev/null +++ b/tests/codeblocks/javascript/async_function.txt @@ -0,0 +1,24 @@ + 0 module `` + 1 function `const asyncFunction = async () =>` + 2 block_delimiter `{` + 2 statement `return` + 3 code `new Promise((resolve, reject) =>` + 4 block_delimiter `{` + 4 code `setTimeout(() =>` + 5 block_delimiter `{` + 5 code `resolve` + 6 code `('Async function resolved')` + 6 block_delimiter `;` + 5 block_delimiter `}` + 5 code `,` + 5 code `1000` + 5 block_delimiter `)` + 5 block_delimiter `;` + 4 block_delimiter `}` + 4 block_delimiter `)` + 3 block_delimiter `;` + 2 block_delimiter `}` + 2 block_delimiter `;` + 1 code `asyncFunction().then(console.log).catch` + 2 code `(console.error)` + 2 block_delimiter `;` diff --git a/tests/codeblocks/javascript/treesitter_types.js b/tests/codeblocks/javascript/treesitter_types.js new file mode 100644 index 00000000..6dd78e3b --- /dev/null +++ b/tests/codeblocks/javascript/treesitter_types.js @@ -0,0 +1,93 @@ +// 1. Variable Declarations +let number = 10; +const PI = 3.14; +var oldSchoolVariable = 'This is old style'; + +// 2. Function Declaration +function greet(name) { + return `Hello, ${name}!`; +} + +// 3. Arrow Function +const square = (x) => x * x; + +// 4. Class Declaration +class Person { + constructor(name, age) { + this.name = name; + this.age = age; + } + + introduce() { + return `Hi, I'm ${this.name} and I'm ${this.age} years old.`; + } +} + +// 5. Object Literal +const obj = { + key: 'value', + method() { + return 'This is a method'; + } +}; + +// 6. Array Declaration +const array = [1, 2, 3, 4]; + +// 7. Loops +for (let i = 0; i < array.length; i++) { + console.log(array[i]); +} + +array.forEach((element) => { + console.log(element); +}); + +// 8. Conditional +if (number > 5) { + console.log('Number is greater than 5'); +} else if (number === 5) { + console.log('Number is 5 '); +} else { + console.log('Number is smaller than 5'); +} + +if (number > 5) + console.log('Number is greater than 5'); +else + console.log('Number is 5 or smaller'); + +// 9. Switch-case +switch (number) { + case 10: + console.log('Number is 10'); + break; + default: + console.log('Number is not 10'); + break; +} + +// 10. Try-catch +try { + throw new Error('This is an error'); +} catch (error) { + console.error('Caught an error:', error.message); +} + + +// 12. Destructuring +const { key } = obj; +const [firstElement] = array; + +// 13. Spread/Rest +const newObj = { ...obj, newKey: 'newValue' }; +const newArray = [...array, 5]; + +// 14. Template Literals +const message = `This is a number: ${number}`; + +// 15. Import/Export (common in modules) +export { greet }; +import { greet } from './path-to-this-file'; + +console.log('Demonstration complete.'); \ No newline at end of file diff --git a/tests/codeblocks/javascript/treesitter_types.txt b/tests/codeblocks/javascript/treesitter_types.txt new file mode 100644 index 00000000..a55a7a72 --- /dev/null +++ b/tests/codeblocks/javascript/treesitter_types.txt @@ -0,0 +1,104 @@ + 0 Module `` + 1 Comment `// 1. Variable Declarations (_1_Variable_Declaratio)` + 1 Assignment `let number = (number)` + 2 Code `10 (number.10)` + 2 Code `; (number._)` + 1 Assignment `const PI = (PI)` + 2 Code `3.14 (PI.3_14)` + 2 Code `; (PI._)` + 1 Code `var oldSchoolVariable = 'This is old style'; (var_oldSchoolVariable_)` + 1 Comment `// 2. Function Declaration (_2_Function_Declaratio)` + 1 Function `function greet(name) (greet)` + 2 Code `{ (greet._)` + 2 Statement `return (greet.return)` + 3 Code ``Hello, ${name}!` (greet.return._Hello_name_)` + 3 Code `; (greet.return._)` + 2 Code `} (greet.None_1)` + 1 Comment `// 3. Arrow Function (_3_Arrow_Function)` + 1 Function `const square = (x) => (square)` + 2 Code `x * x (square.x_x)` + 2 Code `; (square._)` + 1 Comment `// 4. Class Declaration (_4_Class_Declaration)` + 1 Class `class Person (Person)` + 2 Code `{ (Person._)` + 2 Constructor `constructor(name, age) (Person.constructor)` + 3 Code `{ (Person.constructor._)` + 3 Assignment `this.name = name; (Person.constructor.this_name_name_)` + 3 Assignment `this.age = age; (Person.constructor.this_age_age_)` + 3 Code `} (Person.constructor.None_1)` + 2 Function `introduce() (Person.introduce)` + 3 Code `{ (Person.introduce._)` + 3 Statement `return (Person.introduce.return)` + 4 Code ``Hi, I'm ${this.name} and I'm ${this.age} years old.` (Person.introduce.return._Hi_I_m_this_name_and)` + 4 Code `; (Person.introduce.return._)` + 3 Code `} (Person.introduce.None_1)` + 2 Code `} (Person.None_1)` + 1 Comment `// 5. Object Literal (_5_Object_Literal)` + 1 Assignment `const obj = (obj)` + 2 Code `{\n key: 'value',\n method() {\n return 'This is a method';\n }\n} (obj._)` + 2 Code `; (obj.None_1)` + 1 Comment `// 6. Array Declaration (_6_Array_Declaration)` + 1 Assignment `const array = (array)` + 2 Code `[1, 2, 3, 4] (array._1_2_3_4_)` + 2 Code `; (array._)` + 1 Comment `// 7. Loops (_7_Loops)` + 1 Statement `for (let i = 0; i < array.length; i++) (for_let_i_0_i_array)` + 2 Code `{\n console.log(array[i]);\n} (for_let_i_0_i_array._)` + 1 Call `array.forEach((element) => (array_forEach_element_)` + 2 Code `{ (array_forEach_element_._)` + 2 Call `console.log(element); (array_forEach_element_.console_log_element_)` + 2 Code `} (array_forEach_element_.None_1)` + 2 Code `) (array_forEach_element_.None_2)` + 1 Comment `// 8. Conditional (_8_Conditional)` + 1 Statement `if (number > 5) (if_number_5_)` + 2 Code `{\n console.log('Number is greater than 5');\n} (if_number_5_._)` + 2 Statement `else if (number === 5) (if_number_5_.else_if_number_5_)` + 3 Code `{\n console.log('Number is 5 ');\n} (if_number_5_.else_if_number_5_._)` + 3 Statement `else (if_number_5_.else_if_number_5_.else)` + 4 Code `{\n console.log('Number is smaller than 5');\n} (if_number_5_.else_if_number_5_.else._)` + 1 Statement `if (number > 5) (None_2)` + 2 Call `console.log (None_2.console_log)` + 3 Code `( (None_2.console_log._)` + 3 Code `'Number is greater than 5' (None_2.console_log._Number_is_greater_than_5)` + 3 Code `) (None_2.console_log.None_2)` + 2 Code `; (None_2._)` + 2 Statement `else (None_2.else)` + 3 Call `console.log (None_2.else.console_log)` + 4 Code `( (None_2.else.console_log._)` + 4 Code `'Number is 5 or smaller' (None_2.else.console_log._Number_is_5_or_smaller_)` + 4 Code `) (None_2.else.console_log.None_2)` + 3 Code `; (None_2.else._)` + 1 Comment `// 9. Switch-case (_9_Switch_case)` + 1 Statement `switch (number) (switch_number_)` + 2 Code `{ (switch_number_._)` + 2 Code `case 10:\n console.log('Number is 10');\n break; (switch_number_.case_10_)` + 2 Code `default:\n console.log('Number is not 10');\n break; (switch_number_.default_)` + 2 Code `} (switch_number_.None_3)` + 1 Comment `// 10. Try-catch (_10_Try_catch)` + 1 Code `try {\n throw new Error('This is an error');\n} catch (error) {\n console.error('Caught an error:', error.message);\n} (try_)` + 1 Comment `// 12. Destructuring (_12_Destructuring)` + 1 Assignment `const { key } = (key)` + 2 Code `obj (key.obj)` + 2 Code `; (key._)` + 1 Assignment `const [firstElement] = (const_firstElement_)` + 2 Code `array (const_firstElement_.array)` + 2 Code `; (const_firstElement_._)` + 1 Comment `// 13. Spread/Rest (_13_Spread_Rest)` + 1 Assignment `const newObj = (newObj)` + 2 Code `{ ...obj, newKey: 'newValue' } (newObj._obj_newKey_newVal)` + 2 Code `; (newObj._)` + 1 Assignment `const newArray = (newArray)` + 2 Code `[...array, 5] (newArray._array_5_)` + 2 Code `; (newArray._)` + 1 Comment `// 14. Template Literals (_14_Template_Literals)` + 1 Assignment `const message = (message)` + 2 Code ``This is a number: ${number}` (message._This_is_a_number_numb)` + 2 Code `; (message._)` + 1 Comment `// 15. Import/Export (common in modules) (_15_Import_Export_com)` + 1 Code `export { greet }; (export_greet_)` + 1 Import `import { greet } from './path-to-this-file'; (import_greet_from_)` + 1 Call `console.log (console_log)` + 2 Code `( (console_log._)` + 2 Code `'Demonstration complete.' (console_log._Demonstration_complete_)` + 2 Code `) (console_log.None_2)` + 1 Code `; (_)` diff --git a/tests/codeblocks/test_javascript_parser.py b/tests/codeblocks/test_javascript_parser.py new file mode 100644 index 00000000..f4d571f0 --- /dev/null +++ b/tests/codeblocks/test_javascript_parser.py @@ -0,0 +1,479 @@ +from moatless.codeblocks import CodeBlockType +from moatless.codeblocks.parser.javascript import JavaScriptParser + +parser = JavaScriptParser("javascript", debug=True) + + +def test_javascript_treesitter_types(): + with open("javascript/treesitter_types.js", "r") as f: + content = f.read() + with open("javascript/treesitter_types.txt", "r") as f: + expected_tree = f.read() + + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True, use_colors=True)) + + assert codeblock.to_tree(use_colors=False) == expected_tree + assert codeblock.to_string() == content + + +def test_javascript_async_function(): + with open("javascript/async_function.js", "r") as f: + content = f.read() + with open("javascript/async_function.txt", "r") as f: + expected_tree = f.read() + + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=False)) + + assert codeblock.to_tree(use_colors=False) == expected_tree + assert codeblock.to_string() == content + + +def test_class(): + content = """class Person { + constructor(name, age) { + this.name = name; + this.age = age; + } + + introduce() { + age = this.age; + return `Hi, I'm ${this.name} and I'm ${age} years old.`; + } +}""" + + codeblock = parser.parse(content) + print(codeblock.to_tree(include_references=True, include_parameters=True)) + + assert codeblock.to_string() == content + assert codeblock.to_tree(use_colors=False, include_references=True, include_parameters=True) == """ 0 module `` + 1 class `Person` + 2 constructor `constructor(name, age)` + 3 assignment `this.name` references: (this.name) [local:dependency] -> name + 3 assignment `this.age` references: (this.age) [local:dependency] -> age + 2 function `introduce` + 3 assignment `age` references: (age) [class:dependency] -> age + 3 statement `return` + 4 code ``Hi, I'm ${this.name} and I'm ${age} years old.`` references: () [class:dependency] -> name, () [local:dependency] -> age +""" + + +def test_javascript_object_literal(): + content = """const obj = { + key: 'value', + method() { + return 'This is a method'; + } +}; +""" + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + +def test_if_statement(): + content = """if (number > 5) { + console.log('Number is greater than 5'); +} else if (number === 5) { + console.log('Number is 5 '); +} else { + console.log('Number is smaller than 5'); +} +""" + parser = JavaScriptParser("javascript", debug=True) + + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + + +def test_switch_statement(): + content = """switch (number) { + case 10: + console.log('Number is 10'); + break; + default: + console.log('Number is not 10'); + break; +}""" + + parser = JavaScriptParser("javascript", debug=True) + + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + +def test_old_school_var(): + content = """var foo = 1;""" + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + + +def test_jest_test_suite(): + content = """describe('Accessibility', () => { + it('should not have traceable title', async () => { + await renderFoo(); + await waitFor(() => { + expect( + screen.getByTestId('foo-container') + ).toHaveAttribute('tabIndex', '-1'); + }); + }); +});""" + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True)) + + assert codeblock.to_string() == content + +def test_call_expressions(): + content = """expect() +expect(screen) +expect(screen, foo(), this.bar) +expect(screen.getByTestId('foo-container')) +foo().bar() +expect(screen).toHaveAttribute('tabIndex', '-1');""" + + parser = JavaScriptParser("javascript", debug=True) + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True)) + + assert codeblock.to_string() == content + +def test_function_field_definition(): + content = """class FooForm extends Component { + isContactFormValid = (foo) => + isValidPhone(this.state.mobileNumber) && this.validEmail(this.state.email); +}""" + + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(use_colors=False, debug=True, include_references=True, include_parameters=True)) + + +def test_lexical_declaration_function(): + content = """const isDisabled = () => + this.props.disabled; +""" + + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + + assert codeblock.children[0].type == CodeBlockType.FUNCTION + +def test_field_definition_function(): + content = """class Foo extends Component { + foo = () => { + return ( + "foo" + ); + }; +}; +""" + + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) + + assert codeblock.children[0].type == CodeBlockType.CLASS + assert codeblock.children[0].children[1].type == CodeBlockType.FUNCTION + +def test_map_and_jsx(): + content = """const baz = foo?.map( + ({ name, bar }) => ( + + {name} + + this.setState({ + foo: "", + }) + } + > + + + ) + );""" + + #content = "const baz = foo?.map()" + parser = JavaScriptParser("javascript", debug=True) + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) + +def test_map(): + content = "const baz = foo?.map()" + parser = JavaScriptParser("javascript", debug=True) + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) + +def test_css(): + content = """const Styled = { + Subtitle: styled.p` + color: ${({ theme }) => theme.colors.dark}; + font-size: 0.9em; + `, +};""" + + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) + + assert codeblock.to_string() == content + +def test_constructor(): + content = """class Foo extends Component { + + constructor(props) { + super(props); + const { + foo = '' + } = props; + + this.state = { + bar: null + }; + } +} +""" + + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) + + assert codeblock.to_string() == content + + +def test_assignment(): + content = """this.state = { + bar: null + };""" + + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + + assert codeblock.to_string() == content + +def test_solo_constructor(): + content = """constructor(props) { + this.state = { + foo: foo || '', + // ... other state properties + }; +}""" + + parser = JavaScriptParser("javascript", apply_gpt_tweaks=True) + + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + + assert codeblock.to_string() == content + assert codeblock.type == CodeBlockType.MODULE + assert codeblock.children[0].type == CodeBlockType.CONSTRUCTOR + +def test_solo_function(): + content = """useEffect(() => { + + const foo = async () => { + }; + + foo(); +}, []);""" + + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) + + assert codeblock.to_string() == content + assert codeblock.type == CodeBlockType.MODULE + assert len(codeblock.children) == 1 + assert codeblock.children[0].type == CodeBlockType.FUNCTION + +def test_function_indent(): + content =""" isValid = () => { + return false; + }; + + isInvalid = () => { + return true; + };""" + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + + assert codeblock.type == CodeBlockType.MODULE + assert len(codeblock.children) == 2 + assert codeblock.children[0].type == CodeBlockType.FUNCTION + assert codeblock.children[1].type == CodeBlockType.FUNCTION + assert codeblock.to_string() == content + + +def test_commented_out(): + content = """this.state = { + foo: foo || '', + // ... other state properties + };""" + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + + assert codeblock.type == CodeBlockType.MODULE + + +def test_array_call(): + content = """ +array.forEach((element) => { + console.log(element); +}); +""" + parser = JavaScriptParser("javascript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) + + assert codeblock.children[0].type == CodeBlockType.CALL + assert codeblock.to_string() == content + + +def test_root_functions_indent(): + content = """ + componentDidMount() { + this.setState({ + foo: true, + }); + } + + checkFoo = () => { + return false; + };""" + + parser = JavaScriptParser("javascript", apply_gpt_tweaks=True, debug=True) + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + + assert codeblock.type == CodeBlockType.MODULE + assert len(codeblock.children) == 2 + assert codeblock.children[0].type == CodeBlockType.FUNCTION + assert codeblock.children[1].type == CodeBlockType.FUNCTION + + +def test_imports(): + content = """import { myFunction as func } from './myModule'; +import myDefault, { myFunction } from './myModule'; +import * as myModule from './myModule'; +""" + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True)) + + assert codeblock.to_string() == content + + +def test_const_id(): + content = """const foo = await bar({ +});""" + parser = JavaScriptParser("javascript", debug=True) + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) + + assert codeblock.type == CodeBlockType.MODULE + assert len(codeblock.children) == 1 + assert codeblock.children[0].type == CodeBlockType.ASSIGNMENT + + +def test_incorrect_outcommented_code(): + content = """function foo() { + ... +}""" + parser = JavaScriptParser("javascript", apply_gpt_tweaks=False) + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + assert len(codeblock.find_errors()) == 1 + + parser = JavaScriptParser("javascript", apply_gpt_tweaks=True) + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + assert len(codeblock.find_errors()) == 0 + + assert codeblock.children[0].children[1].type == CodeBlockType.COMMENTED_OUT_CODE + +def test_return_empty_root_tag(): + content = """ + return ( + <> + + + );""" + + parser = JavaScriptParser("javascript", apply_gpt_tweaks=False) + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + + +def test_jest_test(): + content = """ + test( + 'context change should prevent bailout of memoized component (useMemo -> ' + + 'no intermediate fiber)', + async () => { + const root = ReactNoop.createRoot(); + expect(root).toMatchRenderedOutput('1'); + }, + ); + + test('multiple contexts are propagated across retries', async () => { + const root = ReactNoop.createRoot(); + expect(root).toMatchRenderedOutput('1'); + }); +""" + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + + test_cases = [x for x in codeblock.children if x.type == CodeBlockType.TEST_CASE] + assert len(test_cases) == 2 + + +def test_real_example_ReactContextPropagation_test(): + with open("javascript/ReactContextPropagation-test.js", "r") as f: + content = f.read() + + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + + print(codeblock.to_string_with_blocks(['', 'ReactLazyContextPropagation.context is propagated across retries (legacy)'], include_references=False, show_commented_out_code_comment=False)) + + blocks = codeblock.get_blocks(has_identifier=True, + include_types=[CodeBlockType.FUNCTION, CodeBlockType.CLASS, + CodeBlockType.TEST_CASE, CodeBlockType.TEST_SUITE]) + + #for block in blocks: + # block_content = block.to_context_string(show_commented_out_code_comment=False) + # tokens = count_tokens(block_content) + # print("path: " + str(block.path_string()) + " , tokens:" + str(tokens)) + + print(len(blocks)) + #assert codeblock.to_string() == content + + + +def test_react(): + with open("/home/albert/repos/stuffs/react/packages/react-dom/src/__tests__/ReactDOMForm-test.js") as f: + content = f.read() + + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + print(codeblock.to_string_with_blocks([''], include_references=False, show_commented_out_code_comment=False)) diff --git a/tests/codeblocks/test_typescript_parser.py b/tests/codeblocks/test_typescript_parser.py new file mode 100644 index 00000000..5a8ef876 --- /dev/null +++ b/tests/codeblocks/test_typescript_parser.py @@ -0,0 +1,445 @@ +from moatless.codeblocks import CodeBlockType +from moatless.codeblocks.parser.typescript import TypeScriptParser + + +def test_typescript_treesitter_types(): + with open("typescript/treesitter_types.ts", "r") as f: + content = f.read() + with open("typescript/treesitter_types_expected.txt", "r") as f: + expected_tree = f.read() + + parser = TypeScriptParser() + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + + assert codeblock.to_tree(use_colors=False) == expected_tree + + +def test_react_tsx(): + with open("typescript/react_component.tsx", "r") as f: + content = f.read() + with open("typescript/react_component_expected.txt", "r") as f: + expected_tree = f.read() + + parser = TypeScriptParser("tsx") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, only_identifiers=False)) + + assert codeblock.to_tree(use_colors=False) == expected_tree + assert codeblock.to_string() == content + + +def test_precomments(): + content = """ +/** + * This hook gives access the [router object](https://nextjs.org/docs/pages/api-reference/functions/use-router#router-object) + * inside the [Pages Router](https://nextjs.org/docs/pages/building-your-application). + * + * Read more: [Next.js Docs: `useRouter`](https://nextjs.org/docs/pages/api-reference/functions/use-router) + */ +export function useRouter(): NextRouter { + const router = React.useContext(RouterContext) + if (!router) { + throw new Error( + 'NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted' + ) + } + + return router +} +""" + parser = TypeScriptParser("tsx", debug=True) + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True)) + + assert codeblock.to_string() == content + + +def test_imports(): + content = """import { myFunction as func } from './myModule'; +import myDefault, { myFunction } from './myModule'; +import * as myModule from './myModule'; +import myModule = require('myModule'); +import * as React from 'react'; +import * as myModule from './myModule'; +""" + parser = TypeScriptParser("tsx", debug=True) + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True, include_references=True)) + + assert codeblock.to_string() == content + +def test_component(): + parser = TypeScriptParser() + codeblock = parser.parse(""" +const MyComponent: React.FC = ({ initialCount = 0 }) => { + const [count, setCount] = useState(initialCount); +} + +""") + print(codeblock.to_tree(debug=True)) + +def test_assignment(): + parser = TypeScriptParser() + codeblock = parser.parse("let a = 1 + 2;") + print(codeblock.to_tree(debug=True)) + +def test_new_date(): + parser = TypeScriptParser() + codeblock = parser.parse("let a = new Date();") + print(codeblock.to_tree(debug=True)) + +def test_type_annotations(): + parser = TypeScriptParser() + codeblock = parser.parse("let myVariable: Array;") + print(codeblock.to_tree(debug=True)) + assert codeblock.to_tree(use_colors=False) == """ 0 module `` + 1 code `myVariable` + 2 block_delimiter `;` +""" + + +def test_nullable_var(): + parser = TypeScriptParser() + codeblock = parser.parse("let myNullableVar: number | null = null;") + print(codeblock.to_tree(debug=False)) + assert codeblock.to_tree(use_colors=False) == """ 0 module `` + 1 code `myNullableVar` + 2 code `=` + 2 code `null` + 2 block_delimiter `;` +""" + +def test_lexical_declaration(): + parser = TypeScriptParser() + codeblock = parser.parse("let myVariable: number;") + assert codeblock.to_tree(use_colors=False, debug=False) == """ 0 module `` + 1 code `let` + 2 code `myVariable` + 2 code `: number` + 2 block_delimiter `;` +""" + + codeblock_assigned = parser.parse("let myVariable: number = 5;") + + assert codeblock_assigned.to_tree(use_colors=False) == """ 0 module `` + 1 code `let myVariable: number` + 2 code `=` + 2 code `5` + 2 block_delimiter `;` +""" + +def test_lexical_declaration_class(): + parser = TypeScriptParser() + codeblock = parser.parse("const Foo = () => {}") + print(codeblock.to_tree(debug=True)) + + assert codeblock.to_tree(use_colors=False, only_identifiers=False) == """ 0 module `` + 1 class `const Foo = () =>` + 2 block_delimiter `{` + 2 block_delimiter `}` +""" + +def test_lexical_declaration_function(): + parser = TypeScriptParser() + codeblock = parser.parse("const getFoo = useCallback(() => {})") + print(codeblock.to_tree(debug=True)) + + assert codeblock.to_tree(use_colors=False) == """ 0 module `` + 1 function `getFoo` + 2 block_delimiter `{` + 2 block_delimiter `}` + 2 block_delimiter `)` +""" + +def test_export_react_component(): + parser = TypeScriptParser() + codeblock = parser.parse("export const Foo: React.FC = ({\n bar \n}) => {}") + print(codeblock.to_tree(debug=True)) + + assert len(codeblock.children) == 1 + assert codeblock.children[0].type == CodeBlockType.CLASS + assert codeblock.children[0].content == "export const Foo: React.FC = ({\n bar \n}) => " + assert codeblock.children[0].identifier == "Foo" + +def test_set_const(): + parser = TypeScriptParser("typescript") + + content = """const SEARCH_CARE_UNITS_THRESHOLD = 5""" + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + + +def test_react_use_functions(): + parser = TypeScriptParser("typescript") + + content = """const handleSearchInput = useMemo(() => { + const handler: React.ChangeEventHandler = ({ target: { value } }) => { + setSearchValue(value); + }; + + return handler; + }, []);""" + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + + content = """const position = useRef();""" + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + + content = """const[sortBy, setSortBy] = useState ();""" + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True)) + + + + +def test_css(): + content = """ +const SeparatorContainer = styled.div` + hr, + span { + flex: 1; + color: ${({ theme }) => theme.colors.muted.dark}; + } + + hr { + flex: 2; + } +`; +""" + + parser = TypeScriptParser("tsx") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + +def test_declarations(): + content = """ +class Foo extends Bar { + constructor() { + super(); + } +} + +interface FooInterface {} + +const fooFunction = () => + console.log('foo');""" + + parser = TypeScriptParser("typescript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + + assert len(codeblock.children) == 3 + assert codeblock.children[0].identifier == "Foo" + assert codeblock.children[0].type == CodeBlockType.CLASS + assert codeblock.children[1].identifier == "FooInterface" + assert codeblock.children[1].type == CodeBlockType.CLASS + assert codeblock.children[2].identifier == "fooFunction" + assert codeblock.children[2].type == CodeBlockType.FUNCTION + +def test_export_declarations(): + content = """ +export class Foo extends Bar { + constructor() { + super(); + } +} + +export interface FooInterface { + foo: 'push' | 'replace'; +} + +export const fooFunction = () => + console.log('foo');""" + + parser = TypeScriptParser("typescript") + codeblock = parser.parse(content) + + print(codeblock.to_tree(debug=True)) + + assert len(codeblock.children) == 3 + assert codeblock.children[0].identifier == "Foo" + assert codeblock.children[0].type == CodeBlockType.CLASS + assert codeblock.children[1].identifier == "FooInterface" + assert codeblock.children[1].type == CodeBlockType.CLASS + assert codeblock.children[2].identifier == "fooFunction" + assert codeblock.children[2].type == CodeBlockType.FUNCTION + + +def test_forwardRef_class(): + content = """const FooComponent = forwardRef( + ( + { bar } + ) => { +}); +""" + parser = TypeScriptParser("tsx") + codeblock = parser.parse(content) + + assert len(codeblock.children) == 2 + assert codeblock.children[0].type == CodeBlockType.CLASS + assert codeblock.children[0].identifier == "FooComponent" + assert codeblock.children[0].content == "const FooComponent = forwardRef(\n (\n { bar }\n ) =>" + assert codeblock.to_string() == content + +def test_console_log(): + content = """console.log('foo');""" + parser = TypeScriptParser("typescript") + codeblock = parser.parse(content) + + assert codeblock.to_tree(use_colors=False) == """ 0 module `` + 1 code `console.log` + 2 block_delimiter `(` + 2 code `'foo'` + 2 block_delimiter `)` + 2 block_delimiter `;` +""" + + +def test_jest_test(): + content = """ +describe('Foo', () => { + let FooMock: SpyInstance; + + beforeAll(() => { + FooMock = vi.spyOn(FooTable, 'FooTable'); + }); + + beforeEach(vi.clearAllMocks); + + afterAll(vi.resetAllMocks); + + it('should do something', () => { + renderWithProviders(); + expect(FooMock).toHaveBeenCalled(); + }); + + test('should do something else', () => { + renderWithProviders(); + expect(FooMock).toHaveBeenCalled(); + }); + +}); +""" + parser = TypeScriptParser("tsx") + codeblock = parser.parse(content) + print(codeblock.to_tree(only_identifiers=False, debug=True)) + + assert codeblock.children[0].type == CodeBlockType.TEST_SUITE + assert codeblock.children[0].identifier == "Foo" + + test_cases = [x for x in codeblock.children[0].children if x.type == CodeBlockType.TEST_CASE] + assert len(test_cases) == 2 + +def test_return_tsx_elements(): + content = """ +export const DeleteUser = ({ id }: DeleteUserProps) => { + return ( + Delete} + confirmButton={ + + } + /> + ); +}; +""" + parser = TypeScriptParser("tsx", debug=True) + codeblock = parser.parse(content) + print(codeblock.to_tree(only_identifiers=True, include_parameters=True, debug=True, include_references=True)) + + +def test_react_component(): + content = """ +export const Button = React.forwardRef( + ( + { + type = 'button', + className = '', + variant = 'primary', + size = 'md', + isLoading = false, + startIcon, + endIcon, + ...props + }, + ref + ) => { + return ( + + ); + } +); +""" + parser = TypeScriptParser("tsx", debug=True) + codeblock = parser.parse(content) + print(codeblock.to_tree(only_identifiers=True, include_parameters=True, debug=True, include_references=True)) + + + +def test_const_class(): + content = """ +export const MDPreview = ({ value = '' }: MDPreviewProps) => { + return ( +
+ ); +}; +""" + + parser = TypeScriptParser("tsx", debug=True) + codeblock = parser.parse(content) + print(codeblock.to_tree(only_identifiers=True, include_parameters=True, debug=True, include_references=True)) + +def test_export_and_return_call(): + content = """ +export const createComment = ({ data }: CreateCommentDTO): Promise => { + return axios.post('/comments', data); +};""" + + parser = TypeScriptParser(debug=True) + codeblock = parser.parse(content) + print(codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) + +def test_return_call(): + content = """ + return useMutation({ + onMutate: async (newComment) => { + const previousComments = queryClient.getQueryData(['comments', discussionId]); + return { previousComments }; + }, + ...config, + mutationFn: createComment, + });""" + + parser = TypeScriptParser(debug=True) + codeblock = parser.parse(content) + print( + codeblock.to_tree(debug=True, include_references=True, include_parameters=True)) diff --git a/tests/codeblocks/typescript/jsx_elements.tsx b/tests/codeblocks/typescript/jsx_elements.tsx new file mode 100644 index 00000000..917e800f --- /dev/null +++ b/tests/codeblocks/typescript/jsx_elements.tsx @@ -0,0 +1,23 @@ +const TodoApp: React.FC = () => { + return ( +
+

Todo App

end_block +
+ setInput(e.target.value)} + placeholder="Add a todo..." + /> + +
+ {todos.map(todo => ( + + ))} +
+ ); +}; \ No newline at end of file diff --git a/tests/codeblocks/typescript/react_component.tsx b/tests/codeblocks/typescript/react_component.tsx new file mode 100644 index 00000000..61ff0289 --- /dev/null +++ b/tests/codeblocks/typescript/react_component.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; + +interface MyComponentProps { + initialCount?: number; +} + +const MyComponent: React.FC = ({ initialCount = 0 }) => { + const [count, setCount] = useState(initialCount); + + const resetCount = () => { + setCount(initialCount); + }; + + return ( +
+

You clicked {count} times

+ +
+ ); +} + +export default MyComponent; diff --git a/tests/codeblocks/typescript/react_component_expected.txt b/tests/codeblocks/typescript/react_component_expected.txt new file mode 100644 index 00000000..5b46c8be --- /dev/null +++ b/tests/codeblocks/typescript/react_component_expected.txt @@ -0,0 +1,42 @@ + 0 module `` + 1 import `import React, { useState } from 'react';` + 1 class `interface MyComponentProps` + 2 block_delimiter `{` + 2 code `initialCount?: number` + 2 block_delimiter `;` + 2 block_delimiter `}` + 1 class `const MyComponent: React.FC = ({ initialCount = 0 }) =>` + 2 block_delimiter `{` + 2 code `const [count, setCount]` + 3 code `=` + 3 code `useState` + 4 code `(initialCount)` + 3 block_delimiter `;` + 2 function `const resetCount = () =>` + 3 block_delimiter `{` + 3 code `setCount` + 4 code `(initialCount)` + 4 block_delimiter `;` + 3 block_delimiter `}` + 3 block_delimiter `;` + 2 statement `return` + 3 block_delimiter `(` + 3 code `
` + 4 code `` + 4 code `

You clicked` + 5 block_delimiter `{` + 5 code `count` + 5 block_delimiter `}` + 5 code `times` + 5 code `

` + 4 code `` + 4 code `` + 4 code `` + 4 code `
` + 3 block_delimiter `)` + 3 block_delimiter `;` + 2 block_delimiter `}` + 1 code `export default MyComponent;` + 1 space `` diff --git a/tests/codeblocks/typescript/todo.md b/tests/codeblocks/typescript/todo.md new file mode 100644 index 00000000..5653b170 --- /dev/null +++ b/tests/codeblocks/typescript/todo.md @@ -0,0 +1,166 @@ +# Chunk 1 +```typescript +import React, { useState } from 'react'; + +// ... + +export default TodoApp; +``` + +# Chunk 2 +```typescript +// ... + +interface Todo { + id: number; + text: string; + done: boolean; +} + +// ... +``` + +# Chunk 3 +```typescript +// ... + +interface TodoProps { + todo: Todo; + onDelete: (id: number) => void; + onToggle: (id: number) => void; +} + +// ... +``` + +# Chunk 4 +```typescript +// ... + +const TodoItem: React.FC = ({ todo, onDelete, onToggle }) => { + return ( +
+ onToggle(todo.id)} + > + {todo.text} + + +
+ ); +}; + +// ... +``` + +# Chunk 5 +```typescript +// ... + +const TodoApp: React.FC = () => { + const [todos, setTodos] = useState([]); + const [input, setInput] = useState(""); + + // ... +}; + +// ... +``` + +# Chunk 6 +```typescript +// ... + +const TodoApp: React.FC = () => { + + // ... + + return ( +
+

Todo App

end_block +
+ setInput(e.target.value)} + placeholder="Add a todo..." + /> + +
+ {todos.map(todo => ( + + ))} +
+ ); +}; + +// ... +``` + +# Chunk 7 +```typescript +// ... + +const TodoApp: React.FC = () => { + // ... + + const addTodo = () => { + const newTodo: Todo = { + id: Date.now(), + text: input, + done: false, + }; + + setTodos(prevTodos => [...prevTodos, newTodo]); + setInput(""); + }; + + // ... +}; + +// ... +``` + +# Chunk 8 +```typescript +// ... + +const TodoApp: React.FC = () => { + // ... + + const deleteTodo = (id: number) => { + setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id)); + }; + + // ... +}; + +// ... +``` + +# Chunk 9 +```typescript +// ... + +const TodoApp: React.FC = () => { + // ... + + const toggleTodo = (id: number) => { + setTodos(prevTodos => + prevTodos.map(todo => + todo.id === id ? { ...todo, done: !todo.done } : todo + ) + ); + }; + + // ... +}; + +// ... +``` + diff --git a/tests/codeblocks/typescript/todo.tsx b/tests/codeblocks/typescript/todo.tsx new file mode 100644 index 00000000..9ad85022 --- /dev/null +++ b/tests/codeblocks/typescript/todo.tsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; + +interface Todo { + id: number; + text: string; + done: boolean; +} + +interface TodoProps { + todo: Todo; + onDelete: (id: number) => void; + onToggle: (id: number) => void; +} + +const TodoItem: React.FC = ({ todo, onDelete, onToggle }) => { + return ( +
+ onToggle(todo.id)} + > + {todo.text} + + +
+ ); +}; + +const TodoApp: React.FC = () => { + const [todos, setTodos] = useState([]); + const [input, setInput] = useState(""); + + const addTodo = () => { + const newTodo: Todo = { + id: Date.now(), + text: input, + done: false, + }; + + setTodos(prevTodos => [...prevTodos, newTodo]); + setInput(""); + }; + + const deleteTodo = (id: number) => { + setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id)); + }; + + const toggleTodo = (id: number) => { + setTodos(prevTodos => + prevTodos.map(todo => + todo.id === id ? { ...todo, done: !todo.done } : todo + ) + ); + }; + + return ( +
+

Todo App

end_block +
+ setInput(e.target.value)} + placeholder="Add a todo..." + /> + +
+ {todos.map(todo => ( + + ))} +
+ ); +}; + +export default TodoApp; \ No newline at end of file diff --git a/tests/codeblocks/typescript/todo_add_filter.tsx b/tests/codeblocks/typescript/todo_add_filter.tsx new file mode 100644 index 00000000..bb171c16 --- /dev/null +++ b/tests/codeblocks/typescript/todo_add_filter.tsx @@ -0,0 +1,60 @@ +// ... (rest of the imports and types) + +enum Filter { + All = 'ALL', + Active = 'ACTIVE', + Completed = 'COMPLETED', +} + +const TodoApp: React.FC = () => { + const [todos, setTodos] = useState([]); + const [input, setInput] = useState(""); + const [currentFilter, setCurrentFilter] = useState(Filter.All); + + // ... (rest of the useState logic and methods) + + const filteredTodos = () => { + switch (currentFilter) { + case Filter.Active: + return todos.filter(todo => !todo.done); + case Filter.Completed: + return todos.filter(todo => todo.done); + default: + return todos; + } + }; + + return ( +
+

Todo App

+
+ +
+
+ setInput(e.target.value)} + placeholder="Add a todo..." + /> + +
+ {filteredTodos().map(todo => ( + + ))} +
+ ); +}; + +export default TodoApp; \ No newline at end of file diff --git a/tests/codeblocks/typescript/todo_merged.tsx b/tests/codeblocks/typescript/todo_merged.tsx new file mode 100644 index 00000000..60984a5b --- /dev/null +++ b/tests/codeblocks/typescript/todo_merged.tsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; + +interface Todo { + id: number; + text: string; + done: boolean; +} + +interface TodoProps { + todo: Todo; + onDelete: (id: number) => void; + onToggle: (id: number) => void; +} + +const TodoItem: React.FC = ({ todo, onDelete, onToggle }) => { + return ( +
+ onToggle(todo.id)} + > + {todo.text} + + +
+ ); +}; + +enum Filter { + All = 'ALL', + Active = 'ACTIVE', + Completed = 'COMPLETED', +} + +const TodoApp: React.FC = () => { + const [todos, setTodos] = useState([]); + const [input, setInput] = useState(""); + const [currentFilter, setCurrentFilter] = useState(Filter.All); + + const addTodo = () => { + const newTodo: Todo = { + id: Date.now(), + text: input, + done: false, + }; + + setTodos(prevTodos => [...prevTodos, newTodo]); + setInput(""); + }; + + const deleteTodo = (id: number) => { + setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id)); + }; + + const toggleTodo = (id: number) => { + setTodos(prevTodos => + prevTodos.map(todo => + todo.id === id ? { ...todo, done: !todo.done } : todo + ) + ); + }; + + const filteredTodos = () => { + switch (currentFilter) { + case Filter.Active: + return todos.filter(todo => !todo.done); + case Filter.Completed: + return todos.filter(todo => todo.done); + default: + return todos; + } + }; + + return ( +
+

Todo App

+
+ +
+
+ setInput(e.target.value)} + placeholder="Add a todo..." + /> + +
+ {filteredTodos().map(todo => ( + + ))} +
+ ); +}; + +export default TodoApp; \ No newline at end of file diff --git a/tests/codeblocks/typescript/treesitter_types.ts b/tests/codeblocks/typescript/treesitter_types.ts new file mode 100644 index 00000000..3ee8775b --- /dev/null +++ b/tests/codeblocks/typescript/treesitter_types.ts @@ -0,0 +1,84 @@ +// Importing modules +import { myFunction as func } from './myModule'; +import myDefault, { myFunction } from './myModule'; +import * as myModule from './myModule'; +import myModule = require('myModule'); + +// Exporting modules +export { myFunction }; +export * from './myModule'; + +// Enum declaration and assignment +enum MyEnum { + Value1 = 1, + Value2 = 2 +} + +// Interface declaration and extension +interface IMyBaseInterface { } +interface IMyDerivedInterface extends IMyBaseInterface { } + +// Class declaration, extension, and implementation +abstract class MyBaseClass { + abstract myAbstractMethod(input: string): void; +} + +class MyDerivedClass extends MyBaseClass implements IMyInterface { + public myField: string; + override myAbstractMethod(input: string): void { } + myMethod(arg: string): number { return 0; } +} + +// Function declaration and usage +function myFunction(param1: string, param2: number) { } +let myVariable: typeof myFunction; + +// Variable declaration and usage +let x = 10; +x += 5; +let y = x; +let z = 'Hello'; +let a = 1 + 2; +let myNullableVar: number | null = null; +let myVar = myNullableVar!; +let myVariable: number = 10; + +// Type declaration and usage +type MyType = string | number; +let myVariable: MyType; + +// Try-catch block +try { + // some code +} catch (error) { + console.error(error); +} + +// Console log +console.log("Hello, World!"); + +// New expression +let date = new Date(); + +// Other expressions +let x = (1 + 2) * 3; +let myInstance = new MyClass(); +let myVariable = someValue; +let myVariable = someValue as number; +let myVariable: number | undefined; +function myFunction(...restParams: number[]) { } +let myVariable: Array; +function isString(test: any): test is string { + return typeof test === "string"; +} +let myVariable: typeof someOtherVariable; +type MyLookupType = MyClass['myProperty']; +type MyLiteralType = 'literal'; +let myVariable: number; +let myVariable: string; +let myVariable: { property: string }; +let myVariable: number[]; +let myVariable: [number, string]; +let myVariable: readonly number[]; +let myVariable: number | string; +let myVariable: (param: string) => number; \ No newline at end of file diff --git a/tests/codeblocks/typescript/treesitter_types_expected.txt b/tests/codeblocks/typescript/treesitter_types_expected.txt new file mode 100644 index 00000000..ccc9bc2a --- /dev/null +++ b/tests/codeblocks/typescript/treesitter_types_expected.txt @@ -0,0 +1,178 @@ + 0 module `` + 1 comment `// Importing modules` + 1 import `import` + 2 code `{` + 3 code `myFunction as func` + 3 block_delimiter `}` + 2 code `from` + 2 code `'./myModule'` + 2 block_delimiter `;` + 1 import `import` + 2 code `myDefault, {` + 3 code `myFunction` + 3 block_delimiter `}` + 2 code `from` + 2 code `'./myModule'` + 2 block_delimiter `;` + 1 import `import` + 2 code `* as myModule` + 2 code `from` + 2 code `'./myModule'` + 2 block_delimiter `;` + 1 import `import myModule = require('myModule');` + 1 comment `// Exporting modules` + 1 code `export { myFunction };` + 1 code `export * from './myModule';` + 1 comment `// Enum declaration and assignment` + 1 class `enum MyEnum` + 2 block_delimiter `{` + 2 code `Value1 = 1` + 2 code `,` + 2 code `Value2 = 2` + 2 block_delimiter `}` + 1 comment `// Interface declaration and extension` + 1 class `interface IMyBaseInterface` + 2 block_delimiter `{` + 2 block_delimiter `}` + 1 class `interface IMyDerivedInterface extends IMyBaseInterface` + 2 block_delimiter `{` + 2 block_delimiter `}` + 1 comment `// Class declaration, extension, and implementation` + 1 class `abstract class MyBaseClass` + 2 block_delimiter `{` + 2 function `abstract myAbstractMethod(input: string): void` + 2 block_delimiter `;` + 2 block_delimiter `}` + 1 class `class MyDerivedClass extends MyBaseClass implements IMyInterface` + 2 block_delimiter `{` + 2 code `public myField: string` + 2 block_delimiter `;` + 2 function `override myAbstractMethod(input: string): void` + 3 block_delimiter `{` + 3 block_delimiter `}` + 2 function `myMethod(arg: string): number` + 3 block_delimiter `{` + 3 statement `return` + 4 code `0` + 4 block_delimiter `;` + 3 block_delimiter `}` + 2 block_delimiter `}` + 1 comment `// Function declaration and usage` + 1 function `function myFunction(param1: string, param2: number)` + 2 block_delimiter `{` + 2 block_delimiter `}` + 1 code `let myVariable: typeof myFunction` + 2 block_delimiter `;` + 1 comment `// Variable declaration and usage` + 1 code `let x` + 2 code `=` + 2 code `10` + 2 block_delimiter `;` + 1 code `x += 5;` + 1 code `let y` + 2 code `=` + 2 code `x` + 2 block_delimiter `;` + 1 code `let z` + 2 code `=` + 2 code `'Hello'` + 2 block_delimiter `;` + 1 code `let a` + 2 code `=` + 2 code `1 + 2` + 2 block_delimiter `;` + 1 code `let myNullableVar: number | null` + 2 code `=` + 2 code `null` + 2 block_delimiter `;` + 1 code `let myVar` + 2 code `=` + 2 code `myNullableVar!` + 2 block_delimiter `;` + 1 code `let myVariable: number` + 2 code `=` + 2 code `10` + 2 block_delimiter `;` + 1 comment `// Type declaration and usage` + 1 code `type MyType` + 2 code `=` + 2 code `string | number` + 2 block_delimiter `;` + 1 code `let myVariable: MyType` + 2 block_delimiter `;` + 1 comment `// Try-catch block` + 1 statement `try` + 2 block_delimiter `{` + 2 comment `// some code` + 2 block_delimiter `}` + 2 code `catch (error)` + 3 block_delimiter `{` + 3 code `console.error` + 4 code `(error)` + 4 block_delimiter `;` + 3 block_delimiter `}` + 1 comment `// Console log` + 1 code `console.log` + 2 code `("Hello, World!")` + 2 block_delimiter `;` + 1 comment `// New expression` + 1 code `let date` + 2 code `=` + 2 code `new Date()` + 2 block_delimiter `;` + 1 comment `// Other expressions` + 1 code `let x` + 2 code `=` + 2 code `(1 + 2) * 3` + 2 block_delimiter `;` + 1 code `let myInstance` + 2 code `=` + 2 code `new MyClass()` + 2 block_delimiter `;` + 1 code `let myVariable` + 2 code `=` + 2 code `someValue` + 2 block_delimiter `;` + 1 code `let myVariable` + 2 code `=` + 2 code `someValue as number` + 2 block_delimiter `;` + 1 code `let myVariable: number | undefined` + 2 block_delimiter `;` + 1 function `function myFunction(...restParams: number[])` + 2 block_delimiter `{` + 2 block_delimiter `}` + 1 code `let myVariable: Array` + 2 block_delimiter `;` + 1 function `function isString(test: any): test is string` + 2 block_delimiter `{` + 2 statement `return` + 3 code `typeof test === "string"` + 3 block_delimiter `;` + 2 block_delimiter `}` + 1 code `let myVariable: typeof someOtherVariable` + 2 block_delimiter `;` + 1 code `type MyLookupType` + 2 code `=` + 2 code `MyClass['myProperty']` + 2 block_delimiter `;` + 1 code `type MyLiteralType` + 2 code `=` + 2 code `'literal'` + 2 block_delimiter `;` + 1 code `let myVariable: number` + 2 block_delimiter `;` + 1 code `let myVariable: string` + 2 block_delimiter `;` + 1 code `let myVariable: { property: string }` + 2 block_delimiter `;` + 1 code `let myVariable: number[]` + 2 block_delimiter `;` + 1 code `let myVariable: [number, string]` + 2 block_delimiter `;` + 1 code `let myVariable: readonly number[]` + 2 block_delimiter `;` + 1 code `let myVariable: number | string` + 2 block_delimiter `;` + 1 code `let myVariable: (param: string) => number` + 2 block_delimiter `;`