Repository: oppia/oppia

Issue:
Add a lint check to check whether the type defs need to be updated
Once the lint checks are in, Change the version of wavesurfer in package.json to `^3.3.3`. (So that we use the latest version of the libs and the typedefs are up to date as well.)

Refer - https://github.com/oppia/oppia/pull/9279#discussion_r422665874


Discussion:
@nishantwrp, Can you please add more info about "Once they are in"? [So that this issue is self-explanatory and it's not necessarily required to look into the thread!]
Done
> Once the lint checks are in

It's still not clear:
- Which lint test?

I would suggest writing it this way:

------
**Current situation:**
Currently, we have removed `^` so that ... <reason>... as we expect to update/verify typedef in <filename> whenever we update ....

**Expected situation:**
To resolve this issue we need to add '^' over here [Link] once we have a lint check which will allow/inform/warm ......

-------

I think the issue name describes the lint check.

Resolving patch:
diff --git a/scripts/linters/linter_utils.py b/scripts/linters/linter_utils.py
--- a/scripts/linters/linter_utils.py
+++ b/scripts/linters/linter_utils.py
@@ -31,6 +31,9 @@
 
 import python_utils
 
+MESSAGE_TYPE_SUCCESS = 'SUCCESS'
+MESSAGE_TYPE_FAILED = 'FAILED'
+
 
 def memoize(func):
     """Decorator which provides thread-safe, cached-access to the return values
diff --git a/scripts/linters/pre_commit_linter.py b/scripts/linters/pre_commit_linter.py
--- a/scripts/linters/pre_commit_linter.py
+++ b/scripts/linters/pre_commit_linter.py
@@ -67,6 +67,7 @@
 from . import html_linter
 from . import js_ts_linter
 from . import python_linter
+from . import third_party_typings_linter
 from .. import common
 from .. import concurrent_task_utils
 from .. import install_third_party_libs
@@ -553,6 +554,10 @@ def main(args=None):
     lint_messages += codeowner_linter.check_codeowner_file(
         verbose_mode_enabled)
 
+    lint_messages += (
+        third_party_typings_linter.check_third_party_libs_type_defs(
+            verbose_mode_enabled))
+
     _print_complete_summary_of_lint_messages(lint_messages)
 
     errors_stacktrace = concurrent_task_utils.ALL_ERRORS
diff --git a/scripts/linters/third_party_typings_linter.py b/scripts/linters/third_party_typings_linter.py
new file mode 100644
--- /dev/null
+++ b/scripts/linters/third_party_typings_linter.py
@@ -0,0 +1,157 @@
+# coding: utf-8
+#
+# Copyright 2020 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Lint checks for third party type definitions."""
+
+from __future__ import absolute_import  # pylint: disable=import-only-modules
+from __future__ import unicode_literals  # pylint: disable=import-only-modules
+
+import json
+import os
+import sys
+
+import python_utils
+from . import linter_utils
+
+MANIFEST_JSON_FILE_PATH = os.path.join(os.getcwd(), 'manifest.json')
+PACKAGE_JSON_FILE_PATH = os.path.join(os.getcwd(), 'package.json')
+_TYPE_DEFS_FILE_EXTENSION_LENGTH = len('.d.ts')
+_DEPENDENCY_SOURCE_MANIFEST = 'manifest.json'
+_DEPENDENCY_SOURCE_PACKAGE = 'package.json'
+
+THIRD_PARTY_LIBS = [
+    {
+        'name': 'Guppy',
+        'dependency_key': 'guppy',
+        'dependency_source': _DEPENDENCY_SOURCE_MANIFEST,
+        'type_defs_filename_prefix': 'guppy-defs-'
+    },
+    {
+        'name': 'Skulpt',
+        'dependency_key': 'skulpt-dist',
+        'dependency_source': _DEPENDENCY_SOURCE_MANIFEST,
+        'type_defs_filename_prefix': 'skulpt-defs-'
+    },
+    {
+        'name': 'Math Expressions',
+        'dependency_key': 'mathExpressions',
+        'dependency_source': _DEPENDENCY_SOURCE_MANIFEST,
+        'type_defs_filename_prefix': 'math-expressions-defs-'
+    },
+    {
+        'name': 'MIDI',
+        'dependency_key': 'midiJs',
+        'dependency_source': _DEPENDENCY_SOURCE_MANIFEST,
+        'type_defs_filename_prefix': 'midi-defs-'
+    },
+    {
+        'name': 'Wavesurfer',
+        'dependency_key': 'wavesurfer.js',
+        'dependency_source': _DEPENDENCY_SOURCE_PACKAGE,
+        'type_defs_filename_prefix': 'wavesurfer-defs-'
+    },
+    {
+        'name': 'Nerdamer',
+        'dependency_key': 'nerdamer',
+        'dependency_source': _DEPENDENCY_SOURCE_PACKAGE,
+        'type_defs_filename_prefix': 'nerdamer-defs-'
+    }
+]
+
+
+def check_third_party_libs_type_defs(verbose_mode_enabled):
+    """Checks the type definitions for third party libs
+    are up to date.
+    """
+    if verbose_mode_enabled:
+        python_utils.PRINT('Starting type defs check')
+        python_utils.PRINT('----------------------------------------')
+
+    with linter_utils.redirect_stdout(sys.stdout):
+        failed = False
+        summary_messages = []
+
+        manifest = json.load(python_utils.open_file(
+            MANIFEST_JSON_FILE_PATH, 'r'))['dependencies']['frontend']
+
+        package = json.load(python_utils.open_file(
+            PACKAGE_JSON_FILE_PATH, 'r'))['dependencies']
+
+        files_in_typings_dir = os.listdir(
+            os.path.join(os.getcwd(), 'typings'))
+
+        for third_party_lib in THIRD_PARTY_LIBS:
+            lib_dependency_source = third_party_lib['dependency_source']
+
+            if lib_dependency_source == _DEPENDENCY_SOURCE_MANIFEST:
+                lib_version = (
+                    manifest[third_party_lib['dependency_key']]['version'])
+
+            elif lib_dependency_source == _DEPENDENCY_SOURCE_PACKAGE:
+                lib_version = package[third_party_lib['dependency_key']]
+
+                if lib_version[0] == '^':
+                    lib_version = lib_version[1:]
+
+            prefix_name = third_party_lib['type_defs_filename_prefix']
+
+            files_with_prefix_name = []
+
+            files_with_prefix_name = [
+                file_name for file_name in files_in_typings_dir
+                if file_name.startswith(prefix_name)]
+
+            if len(files_with_prefix_name) > 1:
+                python_utils.PRINT(
+                    'There are multiple type definitions for %s in the typings '
+                    'dir.' % third_party_lib['name'])
+                python_utils.PRINT('')
+                failed = True
+            elif len(files_with_prefix_name) == 0:
+                python_utils.PRINT(
+                    'There are no type definitions for %s in the typings '
+                    'dir.' % third_party_lib['name'])
+                python_utils.PRINT('')
+                failed = True
+            else:
+                type_defs_filename = files_with_prefix_name[0]
+
+                type_defs_version = type_defs_filename[
+                    len(prefix_name): -_TYPE_DEFS_FILE_EXTENSION_LENGTH]
+
+                if lib_version != type_defs_version:
+                    python_utils.PRINT(
+                        'Type definitions for %s are not up to date. The '
+                        'current version of %s is %s and the type definitions '
+                        'are for version %s. Please refer typings/README.md '
+                        'for more details.' % (
+                            third_party_lib['name'], third_party_lib['name'],
+                            lib_version, type_defs_version))
+                    python_utils.PRINT('')
+                    failed = True
+
+        if failed:
+            summary_message = (
+                '%s  Third party type defs check failed, see messages '
+                'above for more detail.' % linter_utils.MESSAGE_TYPE_FAILED)
+        else:
+            summary_message = '%s  Third party type defs check passed' % (
+                linter_utils.MESSAGE_TYPE_SUCCESS)
+            python_utils.PRINT(summary_message)
+
+        summary_messages.append(summary_message)
+
+    return summary_messages


Test patch:
diff --git a/scripts/linters/third_party_typings_linter_test.py b/scripts/linters/third_party_typings_linter_test.py
new file mode 100644
--- /dev/null
+++ b/scripts/linters/third_party_typings_linter_test.py
@@ -0,0 +1,149 @@
+# coding: utf-8
+#
+# Copyright 2020 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unit tests for third_party_typings_linter.py."""
+
+from __future__ import absolute_import  # pylint: disable=import-only-modules
+from __future__ import unicode_literals  # pylint: disable=import-only-modules
+
+import os
+
+from core.tests import test_utils
+
+import python_utils
+
+from . import linter_utils
+from . import third_party_typings_linter
+
+
+class ThirdPartyTypingsLinterTests(test_utils.GenericTestBase):
+    """Tests for the third_party_typings_linter."""
+
+    def setUp(self):
+        super(ThirdPartyTypingsLinterTests, self).setUp()
+        self.verbose_mode_enabled = False
+        self.manifest_file = python_utils.string_io(
+            buffer_value='{\"dependencies\":{\"frontend\":{\"guppy\":'
+            '{\"version\": \"0.1\"},\"skulpt-dist\":{\"version\": \"0.2\"}'
+            ',\"mathExpressions\":{\"version\": \"0.3\"},\"midiJs\":'
+            '{\"version\": \"0.4\"}}}}')
+        self.package_file = python_utils.string_io(
+            buffer_value='{\"dependencies\":{\"wavesurfer.js\":\"0.5\",'
+            '\"nerdamer\":\"^0.6\"}}')
+        self.files_in_typings_dir = [
+            'guppy-defs-0.1.d.ts',
+            'skulpt-defs-0.2.d.ts',
+            'math-expressions-defs-0.3.d.ts',
+            'midi-defs-0.4.d.ts',
+            'wavesurfer-defs-0.5.d.ts',
+            'nerdamer-defs-0.6.d.ts'
+        ]
+        self.print_arr = []
+        def mock_open_file(path, unused_permissions):
+            if path == third_party_typings_linter.MANIFEST_JSON_FILE_PATH:
+                return self.manifest_file
+            elif path == third_party_typings_linter.PACKAGE_JSON_FILE_PATH:
+                return self.package_file
+        def mock_listdir(unused_path):
+            return self.files_in_typings_dir
+        def mock_print(msg):
+            self.print_arr.append(msg)
+        self.open_file_swap = self.swap(
+            python_utils, 'open_file', mock_open_file)
+        self.listdir_swap = self.swap(os, 'listdir', mock_listdir)
+        self.print_swap = self.swap(python_utils, 'PRINT', mock_print)
+
+
+    def test_check_third_party_libs_type_defs(self):
+        expected_summary_messages = [
+            '%s  Third party type defs check passed' % (
+                linter_utils.MESSAGE_TYPE_SUCCESS)]
+        with self.open_file_swap, self.listdir_swap:
+            summary_messages = (
+                third_party_typings_linter.check_third_party_libs_type_defs(
+                    self.verbose_mode_enabled))
+            self.assertEqual(summary_messages, expected_summary_messages)
+
+    def test_check_third_party_libs_type_defs_verbose(self):
+        self.verbose_mode_enabled = True
+        expected_summary_messages = [
+            '%s  Third party type defs check passed' % (
+                linter_utils.MESSAGE_TYPE_SUCCESS)]
+        with self.open_file_swap, self.listdir_swap:
+            summary_messages = (
+                third_party_typings_linter.check_third_party_libs_type_defs(
+                    self.verbose_mode_enabled))
+            self.assertEqual(summary_messages, expected_summary_messages)
+
+    def test_check_third_party_libs_type_defs_multiple(self):
+        self.files_in_typings_dir.append('guppy-defs-0.2.d.ts')
+        expected_summary_messages = [
+            '%s  Third party type defs check failed, see messages '
+            'above for more detail.' % (
+                linter_utils.MESSAGE_TYPE_FAILED)]
+        with self.open_file_swap, self.listdir_swap, self.print_swap:
+            summary_messages = (
+                third_party_typings_linter.check_third_party_libs_type_defs(
+                    self.verbose_mode_enabled))
+            self.assertEqual(summary_messages, expected_summary_messages)
+            self.assertTrue(
+                'There are multiple type definitions for Guppy in the '
+                'typings dir.' in self.print_arr)
+
+    def test_check_third_party_libs_type_defs_no_type_defs(self):
+        self.files_in_typings_dir = [
+            'skulpt-defs-0.2.d.ts',
+            'math-expressions-defs-0.3.d.ts',
+            'midi-defs-0.4.d.ts',
+            'wavesurfer-defs-0.5.d.ts',
+            'nerdamer-defs-0.6.d.ts'
+        ]
+        expected_summary_messages = [
+            '%s  Third party type defs check failed, see messages '
+            'above for more detail.' % (
+                linter_utils.MESSAGE_TYPE_FAILED)]
+        with self.open_file_swap, self.listdir_swap, self.print_swap:
+            summary_messages = (
+                third_party_typings_linter.check_third_party_libs_type_defs(
+                    self.verbose_mode_enabled))
+            self.assertEqual(summary_messages, expected_summary_messages)
+            self.assertTrue(
+                'There are no type definitions for Guppy in the '
+                'typings dir.' in self.print_arr)
+
+    def test_check_third_party_libs_type_defs_wrong_version(self):
+        self.files_in_typings_dir = [
+            'guppy-defs-0.2.d.ts',
+            'skulpt-defs-0.2.d.ts',
+            'math-expressions-defs-0.3.d.ts',
+            'midi-defs-0.4.d.ts',
+            'wavesurfer-defs-0.5.d.ts',
+            'nerdamer-defs-0.6.d.ts'
+        ]
+        expected_summary_messages = [
+            '%s  Third party type defs check failed, see messages '
+            'above for more detail.' % (
+                linter_utils.MESSAGE_TYPE_FAILED)]
+        with self.open_file_swap, self.listdir_swap, self.print_swap:
+            summary_messages = (
+                third_party_typings_linter.check_third_party_libs_type_defs(
+                    self.verbose_mode_enabled))
+            self.assertEqual(summary_messages, expected_summary_messages)
+            self.assertTrue(
+                'Type definitions for Guppy are not up to date. The '
+                'current version of Guppy is 0.1 and the type definitions '
+                'are for version 0.2. Please refer typings/README.md '
+                'for more details.' in self.print_arr)