diff --git a/.github/workflows/add_language.yml b/.github/workflows/add_language.yml new file mode 100644 index 0000000000..e72229a044 --- /dev/null +++ b/.github/workflows/add_language.yml @@ -0,0 +1,42 @@ +name: Add language to a rule + +# Workflow runs when manually triggered using the UI or API. +on: + workflow_dispatch: + # Inputs the workflow accepts. + inputs: + rule: + description: 'ID of an existing rule (e.g., S1234).' + required: true + language: + description: 'Language to be added to the rule, (e.g., cfamily)' + required: true + +jobs: + add_language_to_rule: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v2 + with: + persist-credentials: true + ref: master + path: 'rspec' + + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: 'Install Pipenv' + run: | + pip install pipenv + + - name: 'Install rspec-tools' + working-directory: 'rspec/rspec-tools' + run: pipenv install -e . + + - name: 'Add Language' + working-directory: 'rspec/rspec-tools' + run: pipenv run rspec-tools add-lang-to-rule --user ${{ github.actor }} --language "${{ github.event.inputs.language }}" --rule "${{ github.event.inputs.rule }}" diff --git a/rspec-tools/Pipfile b/rspec-tools/Pipfile index ddf2b715c3..69016980e9 100644 --- a/rspec-tools/Pipfile +++ b/rspec-tools/Pipfile @@ -10,6 +10,7 @@ rspec-tools = {editable = true, path = "."} gitpython = "*" pygithub = "*" jsonschema = "*" +fs = "*" [dev-packages] pytest = ">=6.2.2" diff --git a/rspec-tools/Pipfile.lock b/rspec-tools/Pipfile.lock index a8430f780b..8899535b23 100644 --- a/rspec-tools/Pipfile.lock +++ b/rspec-tools/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "307552df2c8cbbd087e576ba8c0bcea21c1151baf7f9eb3076e910a9063f66e4" + "sha256": "a141a126414372441705df189b3013cdadd7fa27f3114dfc07264686c6291204" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,13 @@ ] }, "default": { + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" + }, "attrs": { "hashes": [ "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", @@ -25,11 +32,10 @@ }, "beautifulsoup4": { "hashes": [ - "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35", - "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25", - "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666" + "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf", + "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891" ], - "version": "==4.9.3" + "version": "==4.10.0" }, "bs4": { "hashes": [ @@ -45,27 +51,86 @@ ], "version": "==2021.5.30" }, - "chardet": { + "cffi": { "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", + "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", + "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", + "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", + "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", + "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", + "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", + "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", + "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", + "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", + "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", + "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", + "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", + "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", + "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", + "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", + "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", + "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", + "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", + "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", + "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", + "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", + "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", + "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", + "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", + "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", + "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", + "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", + "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", + "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", + "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", + "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", + "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", + "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", + "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", + "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", + "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", + "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", + "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", + "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", + "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", + "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", + "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", + "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", + "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" ], - "version": "==4.0.0" + "version": "==1.14.6" + }, + "charset-normalizer": { + "hashes": [ + "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", + "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" + ], + "markers": "python_version >= '3'", + "version": "==2.0.6" }, "click": { "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" ], "index": "pypi", - "version": "==7.1.2" + "version": "==8.0.1" }, "deprecated": { "hashes": [ - "sha256:08452d69b6b5bc66e8330adde0a4f8642e969b9e1702904d137eeb29c8ffc771", - "sha256:6d2de2de7931a968874481ef30208fd4e08da39177d61d3d4ebdf4366e7dbca1" + "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d", + "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d" ], - "version": "==1.2.12" + "version": "==1.2.13" + }, + "fs": { + "hashes": [ + "sha256:1d10cc8f9c55fbcf7b23775289a13f6796dca7acd5a135c379f49e87a56a7230", + "sha256:caab4dc1561d63c92f36ee78976f6a4a01381830d8420ce34a78d4f1bb1dc95f" + ], + "index": "pypi", + "version": "==2.4.13" }, "gitdb": { "hashes": [ @@ -76,18 +141,19 @@ }, "gitpython": { "hashes": [ - "sha256:8621a7e777e276a5ec838b59280ba5272dd144a18169c36c903d8b38b99f750a", - "sha256:c5347c81d232d9b8e7f47b68a83e5dc92e7952127133c5f2df9133f2c75a1b29" + "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647", + "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5" ], "index": "pypi", - "version": "==3.1.13" + "version": "==3.1.24" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "version": "==2.10" + "markers": "python_version >= '3'", + "version": "==3.2" }, "jsonschema": { "hashes": [ @@ -97,33 +163,90 @@ "index": "pypi", "version": "==3.2.0" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "version": "==2.20" + }, "pygithub": { "hashes": [ - "sha256:300bc16e62886ca6537b0830e8f516ea4bc3ef12d308e0c5aff8bdbd099173d4", - "sha256:87afd6a67ea582aa7533afdbf41635725f13d12581faed7e3e04b1579c0c0627" + "sha256:1bbfff9372047ff3f21d5cd8e07720f3dbfdaf6462fcaed9d815f528f1ba7283", + "sha256:2caf0054ea079b71e539741ae56c5a95e073b81fa472ce222e81667381b9601b" ], "index": "pypi", - "version": "==1.54.1" + "version": "==1.55" }, "pyjwt": { "hashes": [ - "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", - "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" + "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1", + "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130" ], - "version": "==1.7.1" + "version": "==2.1.0" + }, + "pynacl": { + "hashes": [ + "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4", + "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4", + "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574", + "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", + "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634", + "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", + "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f", + "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505", + "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122", + "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7", + "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", + "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f", + "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96", + "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6", + "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6", + "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514", + "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff", + "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80" + ], + "version": "==1.4.0" }, "pyrsistent": { "hashes": [ - "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" + "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2", + "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7", + "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea", + "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426", + "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710", + "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1", + "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396", + "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2", + "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680", + "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35", + "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427", + "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b", + "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b", + "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f", + "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef", + "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c", + "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4", + "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d", + "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78", + "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b", + "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72" ], - "version": "==0.17.3" + "version": "==0.18.0" + }, + "pytz": { + "hashes": [ + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + ], + "version": "==2021.1" }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], - "version": "==2.25.1" + "version": "==2.26.0" }, "rspec-tools": { "editable": true, @@ -148,16 +271,23 @@ "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc", "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b" ], - "markers": "python_version >= '3.0'", "version": "==2.2.1" }, + "typing-extensions": { + "hashes": [ + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" + ], + "markers": "python_version < '3.10'", + "version": "==3.10.0.2" + }, "urllib3": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" ], - "index": "pypi", - "version": "==1.26.5" + "version": "==1.26.7" }, "wrapt": { "hashes": [ @@ -183,31 +313,32 @@ }, "mypy": { "hashes": [ - "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e", - "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064", - "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c", - "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4", - "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97", - "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df", - "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8", - "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a", - "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56", - "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7", - "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6", - "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5", - "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a", - "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521", - "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564", - "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49", - "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66", - "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a", - "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119", - "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506", - "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c", - "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb" + "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9", + "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a", + "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9", + "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e", + "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2", + "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212", + "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b", + "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885", + "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150", + "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703", + "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072", + "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457", + "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e", + "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0", + "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb", + "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97", + "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8", + "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811", + "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6", + "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de", + "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504", + "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921", + "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d" ], "index": "pypi", - "version": "==0.812" + "version": "==0.910" }, "mypy-extensions": { "hashes": [ @@ -218,17 +349,17 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" ], - "version": "==20.9" + "version": "==21.0" }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "version": "==0.13.1" + "version": "==1.0.0" }, "py": { "hashes": [ @@ -246,11 +377,11 @@ }, "pytest": { "hashes": [ - "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", - "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" ], "index": "pypi", - "version": "==6.2.2" + "version": "==6.2.5" }, "rspec-tools": { "editable": true, @@ -263,48 +394,14 @@ ], "version": "==0.10.2" }, - "typed-ast": { - "hashes": [ - "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", - "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", - "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", - "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", - "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", - "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", - "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", - "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", - "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", - "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", - "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", - "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", - "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", - "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", - "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", - "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", - "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", - "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", - "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", - "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", - "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", - "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", - "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", - "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", - "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", - "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", - "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", - "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", - "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", - "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" - ], - "version": "==1.4.3" - }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], - "version": "==3.10.0.0" + "markers": "python_version < '3.10'", + "version": "==3.10.0.2" } } } diff --git a/rspec-tools/rspec_tools/cli.py b/rspec-tools/rspec_tools/cli.py index b778daf467..d80b1a6809 100644 --- a/rspec-tools/rspec_tools/cli.py +++ b/rspec-tools/rspec_tools/cli.py @@ -5,7 +5,7 @@ from pathlib import Path import click from rspec_tools.checklinks import check_html_links from rspec_tools.errors import RuleNotFoundError, RuleValidationError -from rspec_tools.create_rule import create_new_rule +from rspec_tools.create_rule import create_new_rule, add_language_to_rule from rspec_tools.rules import RulesRepository from rspec_tools.validation.metadata import validate_metadata from rspec_tools.validation.description import validate_section_names, validate_section_levels @@ -37,6 +37,15 @@ def create_rule(languages: str, user: Optional[str]): token = os.environ.get('GITHUB_TOKEN') create_new_rule(languages, token, user) +@cli.command() +@click.option('--language', required=True) +@click.option('--rule', required=True) +@click.option('--user', required=False) +def add_lang_to_rule(language: str, rule: str, user: Optional[str]): + '''Add a new language to rule.''' + token = os.environ.get('GITHUB_TOKEN') + add_language_to_rule(language, rule, token, user) + @cli.command() @click.argument('rules', nargs=-1) diff --git a/rspec-tools/rspec_tools/create_rule.py b/rspec-tools/rspec_tools/create_rule.py index 351e9ce588..55343f2dc2 100644 --- a/rspec-tools/rspec_tools/create_rule.py +++ b/rspec-tools/rspec_tools/create_rule.py @@ -1,6 +1,7 @@ -from rspec_tools.errors import GitError +from rspec_tools.errors import InvalidArgumentError import click import tempfile +import fs from git import Repo from git.remote import PushInfo from github import Github @@ -8,7 +9,7 @@ from github.PullRequest import PullRequest from pathlib import Path from typing import Final, Iterable, Optional, Callable from contextlib import contextmanager -from rspec_tools.utils import parse_and_validate_language_list, get_labels_for_languages +from rspec_tools.utils import parse_and_validate_language_list, get_labels_for_languages, validate_language, get_label_for_language, resolve_rule, swap_metadata_files, is_empty_metadata from rspec_tools.utils import copy_directory_content @@ -45,6 +46,19 @@ def create_new_rule(languages: str, token: str, user: Optional[str]): rule_number = rule_creator.reserve_rule_number() pull_request = rule_creator.create_new_rule_pull_request(authGithub(token), rule_number, lang_list, label_list, user=user) +def add_language_to_rule(language: str, rule: str, token: str, user: Optional[str]): + url = build_github_repository_url(token, user) + config = {} + if user: + config['user.name'] = user + config['user.email'] = f'{user}@users.noreply.github.com' + validate_language(language) + label = get_label_for_language(language) + rule_number = resolve_rule(rule) + with tempfile.TemporaryDirectory() as tmpdirname: + rule_creator = RuleCreator(url, tmpdirname, config) + rule_creator.add_language_pull_request(authGithub(token), rule_number, language, label, user=user) + class RuleCreator: ''' Create a new Rule in a repository following the official Github 'rspec' repository structure.''' MASTER_BRANCH: Final[str] = 'master' @@ -69,7 +83,6 @@ class RuleCreator: for key, value in configuration.items(): split_key = key.split('.') config_writer.set_value(*split_key, value) - def reserve_rule_number(self) -> int: '''Reserve an id on the id counter branch of the repository.''' @@ -77,7 +90,7 @@ class RuleCreator: counter_file_path = Path(self.repository.working_dir).joinpath(self.ID_COUNTER_FILENAME) counter = int(counter_file_path.read_text()) counter_file_path.write_text(str(counter + 1)) - + self.repository.index.add([str(counter_file_path)]) self.repository.index.commit('Increment RSPEC ID counter') @@ -87,6 +100,31 @@ class RuleCreator: click.echo(f'Reserved Rule ID S{counter}') return counter + def add_language_branch(self, rule_number: int, language: str) -> str: + '''Create and move files to add a new language to an existing rule.''' + branch_name = f'rule/S{rule_number}-add-{language}' + with self._current_git_branch(self.MASTER_BRANCH, branch_name): + repo_dir = Path(self.repository.working_dir) + rule_dir = repo_dir.joinpath('rules', f'S{rule_number}') + if not rule_dir.is_dir(): + raise InvalidArgumentError(f"Rule \"S{rule_number}\" does not exist.") + lang_dirs = [d for d in rule_dir.glob('*/') if d.is_dir()] + if 1 == len(list(lang_dirs)) and is_empty_metadata(rule_dir): + swap_metadata_files(rule_dir, lang_dirs[0]) + lang_dir = rule_dir.joinpath(language) + if lang_dir.is_dir(): + lang_url = f"https://github.com/SonarSource/rspec/tree/master/rules/S{rule_number}/{language}" + raise InvalidArgumentError(f"Rule \"S{rule_number}\" is already defined for language {language}. Modify the definition here: {lang_url}.") + lang_dir.mkdir() + + lang_specific_template = self.TEMPLATE_PATH.joinpath('multi_language', 'language_specific') + copy_directory_content(lang_specific_template, lang_dir) + self._fill_in_the_blanks_in_the_template(lang_dir, rule_number) + self.repository.git.add('--all') + self.repository.index.commit(f'Add {language} to rule S{rule_number}') + self.repository.git.push('origin', branch_name) + return branch_name + def create_new_rule_branch(self, rule_number: int, languages: Iterable[str]) -> str: '''Create all the files required for a new rule.''' branch_name = f'rule/add-RSPEC-S{rule_number}' @@ -135,17 +173,13 @@ class RuleCreator: self._fill_in_the_blanks_in_the_template(rule_dir, rule_number) - def create_new_rule_pull_request(self, githubApi: Callable[[Optional[str]], Github], rule_number: int, languages: Iterable[str], labels: Iterable[str], *, user: Optional[str]) -> PullRequest: - branch_name = self.create_new_rule_branch(rule_number, languages) - click.echo(f'Created rule Branch {branch_name}') - + def _create_pull_request(self, githubApi: Callable[[Optional[str]], Github], branch_name: str, title: str, body: str, labels: Iterable[str], user: Optional[str]): repository_url = extract_repository_name(self.origin_url) github = githubApi(user) github_repo = github.get_repo(repository_url) - first_lang = next(iter(languages)) pull_request = github_repo.create_pull( - title=f'Create rule S{rule_number}', - body=f'You can preview this rule [here](https://sonarsource.github.io/rspec/#/rspec/S{rule_number}/{first_lang}) (updated a few minutes after each push).', + title=title, + body=body, head=branch_name, base=self.MASTER_BRANCH, draft=True, maintainer_can_modify=True ) @@ -156,9 +190,33 @@ class RuleCreator: pull_request.add_to_assignees(login) pull_request.add_to_labels(*labels) click.echo(f'Pull request assigned to {login}') - return pull_request + def add_language_pull_request(self, githubApi: Callable[[Optional[str]], Github], rule_number: int, language: str, label: str, user: Optional[str]): + branch_name = self.add_language_branch(rule_number, language) + click.echo(f'Created rule branch {branch_name}') + return self._create_pull_request( + githubApi, + branch_name, + f'Create rule S{rule_number}[{language}]', + f'You can preview this rule [here](https://sonarsource.github.io/rspec/#/rspec/S{rule_number}/{language}) (updated a few minutes after each push).', + [label], + user + ) + + def create_new_rule_pull_request(self, githubApi: Callable[[Optional[str]], Github], rule_number: int, languages: Iterable[str], labels: Iterable[str], *, user: Optional[str]) -> PullRequest: + branch_name = self.create_new_rule_branch(rule_number, languages) + click.echo(f'Created rule branch {branch_name}') + first_lang = next(iter(languages)) + return self._create_pull_request( + githubApi, + branch_name, + f'Create rule S{rule_number}', + f'You can preview this rule [here](https://sonarsource.github.io/rspec/#/rspec/S{rule_number}/{first_lang}) (updated a few minutes after each push).', + labels, + user + ) + @contextmanager def _current_git_branch(self, base_branch: str, new_branch: Optional[str] = None): '''Checkout a given branch before yielding, then revert to the previous branch.''' diff --git a/rspec-tools/rspec_tools/utils.py b/rspec-tools/rspec_tools/utils.py index 44c3172518..5a774abe5d 100644 --- a/rspec-tools/rspec_tools/utils.py +++ b/rspec-tools/rspec_tools/utils.py @@ -1,6 +1,9 @@ from rspec_tools.errors import InvalidArgumentError from pathlib import Path import shutil +import re +import tempfile +import json SUPPORTED_LANGUAGES_FILENAME = '../supported_languages.adoc' LANG_TO_LABEL = {'abap': 'abap', @@ -41,6 +44,19 @@ def copy_directory_content(src:Path, dest:Path): else: shutil.copy2(item, dest) +def swap_metadata_files(dir1:Path, dir2:Path): + meta1 = dir1.joinpath('metadata.json') + meta2 = dir2.joinpath('metadata.json') + with tempfile.TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir).joinpath('metadata.json') + shutil.copy2(meta1, tmp) + shutil.copy2(meta2, meta1) + shutil.copy2(tmp, meta2) + +def is_empty_metadata(rule_dir:Path): + with open(rule_dir.joinpath('metadata.json'), 'r') as meta: + return not json.load(meta) + def load_valid_languages(): with open(SUPPORTED_LANGUAGES_FILENAME, 'r') as supported_langs_file: supported_langs = supported_langs_file.read() @@ -65,7 +81,22 @@ def parse_and_validate_language_list(languages): raise InvalidArgumentError(f"Unsupported language: \"{lang}\". See {SUPPORTED_LANGUAGES_FILENAME} for the list of supported languages.") return lang_list +def validate_language(language): + valid_langs = load_valid_languages() + if language not in valid_langs: + raise InvalidArgumentError(f"Unsupported language: \"{language}\". See {SUPPORTED_LANGUAGES_FILENAME} for the list of supported languages.") + def get_labels_for_languages(lang_list): labels = [LANG_TO_LABEL[lang] for lang in lang_list] return list(set(labels)) +def get_label_for_language(language: str) -> str: + return LANG_TO_LABEL[language] + +def resolve_rule(ruleID: str) -> int: + m = re.search('^S([0-9]{3,4})$', ruleID) + if not m: + raise InvalidArgumentError(f"Unrecognized rule id format: \"{ruleID}\". Rule id must start with an \"S\" followed by 3 or 4 digits.") + else: + return int(m.group(1)) + diff --git a/rspec-tools/tests/resources/rules/S1033/cfamily/comments-and-links.adoc b/rspec-tools/tests/resources/rules/S1033/cfamily/comments-and-links.adoc new file mode 100644 index 0000000000..02f1ba68d2 --- /dev/null +++ b/rspec-tools/tests/resources/rules/S1033/cfamily/comments-and-links.adoc @@ -0,0 +1,3 @@ +=== on 21 Oct 2014, 18:50:16 Ann Campbell wrote: +\[~samuel.mercier] this is an "inconsistent with developer expectations" rule, & so should probably be tied to Reliability & is also likely at least a "pitfall" if not a "bug". + diff --git a/rspec-tools/tests/resources/rules/S1033/cfamily/message.adoc b/rspec-tools/tests/resources/rules/S1033/cfamily/message.adoc new file mode 100644 index 0000000000..b8c61a7584 --- /dev/null +++ b/rspec-tools/tests/resources/rules/S1033/cfamily/message.adoc @@ -0,0 +1,4 @@ +=== Message + +Explicitly invoke the template version of this function. + diff --git a/rspec-tools/tests/resources/rules/S1033/cfamily/metadata.json b/rspec-tools/tests/resources/rules/S1033/cfamily/metadata.json new file mode 100644 index 0000000000..ae443530ad --- /dev/null +++ b/rspec-tools/tests/resources/rules/S1033/cfamily/metadata.json @@ -0,0 +1,27 @@ +{ + "title": "The viable function set for a function call should either contain no function specializations, or only contain function specializations", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "10min" + }, + "tags": [ + "based-on-misra" + ], + "extra": { + "replacementRules": [ + + ], + "legacyKeys": [ + + ] + }, + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-1033", + "sqKey": "S1033", + "scope": "Main", + "defaultQualityProfiles": [ + + ] +} diff --git a/rspec-tools/tests/resources/rules/S1033/cfamily/rule.adoc b/rspec-tools/tests/resources/rules/S1033/cfamily/rule.adoc new file mode 100644 index 0000000000..d4943a7c39 --- /dev/null +++ b/rspec-tools/tests/resources/rules/S1033/cfamily/rule.adoc @@ -0,0 +1,53 @@ +If a function and a specialization of a function template are deemed equivalent after overload resolution, the non-specialized function will be chosen over the function specialization, which may be inconsistent with developer expectations. + + +== Noncompliant Code Example + +---- +void f ( short ); // Example 1 +template void f ( T ); // Example 2 +void b ( short s ) +{ + f ( s ); // Noncompliant - Calls Example 1 + f ( s + 1 ); // Noncompliant - Calls Example 2 +} +---- + + +== Compliant Solution + +---- +void f ( short ); // Example 1 +template void f ( T ); // Example 2 +void b ( short s ) +{ + f<>( s ); // Compliant - Explicitly calls Example 2 + f<>( s + 1 ); // Compliant - Explicitly calls Example 2 +} +---- + + +== Exceptions + +This rule does not apply to copy constructors or copy assignment operators. + + +== See + +* MISRA {cpp}:2008, 14-8-2 + + +ifdef::env-github,rspecator-view[] + +''' +== Implementation Specification +(visible only on this page) + +include::message.adoc[] + +''' +== Comments And Links +(visible only on this page) + +include::comments-and-links.adoc[] +endif::env-github,rspecator-view[] diff --git a/rspec-tools/tests/resources/rules/S1033/metadata.json b/rspec-tools/tests/resources/rules/S1033/metadata.json new file mode 100644 index 0000000000..2c63c08510 --- /dev/null +++ b/rspec-tools/tests/resources/rules/S1033/metadata.json @@ -0,0 +1,2 @@ +{ +} diff --git a/rspec-tools/tests/test_create_rule.py b/rspec-tools/tests/test_create_rule.py index daa4af7d30..4976208f1f 100644 --- a/rspec-tools/tests/test_create_rule.py +++ b/rspec-tools/tests/test_create_rule.py @@ -4,8 +4,10 @@ from pathlib import Path from typing import Optional from unittest.mock import Mock, patch import pytest +import shutil -from rspec_tools.create_rule import RuleCreator, create_new_rule +from rspec_tools.create_rule import RuleCreator, create_new_rule, add_language_to_rule +from rspec_tools.utils import is_empty_metadata @pytest.fixture def git_config(): @@ -16,20 +18,18 @@ def git_config(): } @pytest.fixture -def mock_rspec_repo(tmpdir): +def mock_rspec_repo(tmpdir, mockrules: Path): repo_dir = tmpdir.mkdir("mock_rspec") repo = Repo.init(str(repo_dir)) repo.init() + rules_dir = repo_dir.join('rules') + shutil.copytree(mockrules, rules_dir) with repo.config_writer() as config_writer: config_writer.set_value('user', 'name', 'originuser') config_writer.set_value('user', 'email', 'originuser@mock.mock') - rules_dir = repo_dir.mkdir('rules') - # create a file just to have a "rules" directory - gitignore = rules_dir.join('.gitignore') - gitignore.ensure() - repo.index.add([str(gitignore)]) + repo.git.add('--all') repo.index.commit('init rules') # Create the id counter branch. Note that it is an orphan branch. @@ -175,3 +175,120 @@ def test_create_new_rule_unsupported_language(mockRuleCreator): prMock = mockRuleCreator.return_value.create_new_rule_pull_request with pytest.raises(InvalidArgumentError): create_new_rule('russian,php', 'my token', 'testuser') + + +def test_add_lang_singlelang_nonconventional_rule_create_branch(rule_creator: RuleCreator, mock_rspec_repo: Repo): + '''Test add_language_branch for a single-language rule with metadata lifted to the generic rule level.''' + rule_number = 4727 + language = 'php' + + mock_rspec_repo.git.checkout('master') + orig_rule_dir = Path(mock_rspec_repo.working_dir).joinpath('rules', f'S{rule_number}') + assert(not is_empty_metadata(orig_rule_dir)) # nonconventional: singlelang rule with metadata on the upper level + assert(is_empty_metadata(orig_rule_dir.joinpath('cobol'))) + original_metadata = orig_rule_dir.joinpath('metadata.json').read_text() + + branch = rule_creator.add_language_branch(rule_number, language) + + # Check that the branch was pushed successfully to the origin + mock_rspec_repo.git.checkout(branch) + rule_dir = Path(mock_rspec_repo.working_dir).joinpath('rules', f'S{rule_number}') + assert rule_dir.exists() + lang_dir = rule_dir.joinpath(f'{language}') + assert lang_dir.exists() + + assert rule_dir.joinpath('metadata.json').read_text() == original_metadata + assert(is_empty_metadata(rule_dir.joinpath('cobol'))) + + lang_root = rule_creator.TEMPLATE_PATH.joinpath('multi_language', 'language_specific') + for lang_item in lang_root.glob('**/*'): + if lang_item.is_file(): + expected_content = lang_item.read_text().replace('${RSPEC_ID}', str(rule_number)) + relative_path = lang_item.relative_to(lang_root) + actual_content = rule_dir.joinpath(language, relative_path).read_text() + assert actual_content == expected_content + +def test_add_lang_singlelang_conventional_rule_create_branch(rule_creator: RuleCreator, mock_rspec_repo: Repo): + '''Test add_language_branch for a regular single language rule.''' + rule_number = 1033 + language = 'php' + + mock_rspec_repo.git.checkout('master') + orig_rule_dir = Path(mock_rspec_repo.working_dir).joinpath('rules', f'S{rule_number}') + assert(is_empty_metadata(orig_rule_dir)) # conventional: singlelang rule with metadata on the lang-specific level + assert(not is_empty_metadata(orig_rule_dir.joinpath('cfamily'))) + original_lmetadata = orig_rule_dir.joinpath('cfamily', 'metadata.json').read_text() + + branch = rule_creator.add_language_branch(rule_number, language) + + # Check that the branch was pushed successfully to the origin + mock_rspec_repo.git.checkout(branch) + rule_dir = Path(mock_rspec_repo.working_dir).joinpath('rules', f'S{rule_number}') + assert rule_dir.exists() + lang_dir = rule_dir.joinpath(f'{language}') + assert lang_dir.exists() + + assert rule_dir.joinpath('metadata.json').read_text() == original_lmetadata + assert(is_empty_metadata(rule_dir.joinpath('cfamily'))) + +def test_add_lang_multilang_rule_create_branch(rule_creator: RuleCreator, mock_rspec_repo: Repo): + '''Test add_language_branch for a multi-language rule.''' + rule_number = 120 + language = 'php' + + branch = rule_creator.add_language_branch(rule_number, language) + + # Check that the branch was pushed successfully to the origin + mock_rspec_repo.git.checkout(branch) + rule_dir = Path(mock_rspec_repo.working_dir).joinpath('rules', f'S{rule_number}') + assert rule_dir.exists() + lang_dir = rule_dir.joinpath(f'{language}') + assert lang_dir.exists() + + lang_root = rule_creator.TEMPLATE_PATH.joinpath('multi_language', 'language_specific') + for lang_item in lang_root.glob('**/*'): + if lang_item.is_file(): + expected_content = lang_item.read_text().replace('${RSPEC_ID}', str(rule_number)) + relative_path = lang_item.relative_to(lang_root) + actual_content = rule_dir.joinpath(language, relative_path).read_text() + assert actual_content == expected_content + +@patch('rspec_tools.create_rule.RuleCreator') +def test_add_unsupported_language(mockRuleCreator): + '''Test language validation.''' + mockRuleCreator.return_value = Mock() + mockRuleCreator.return_value.create_new_rule_pull_request = Mock() + prMock = mockRuleCreator.return_value.create_new_rule_pull_request + with pytest.raises(InvalidArgumentError): + add_language_to_rule('russian', 'S1033', 'my token', 'testuser') + +def test_add_language_the_rule_is_already_defined_for(rule_creator: RuleCreator): + '''Test add_language_branch fails when trying to add a langage already added to the rule.''' + with pytest.raises(InvalidArgumentError): + rule_creator.add_language_branch(100, 'cfamily') + +def test_add_language_to_nonexistent_rule(rule_creator: RuleCreator): + '''Test add_language_branch correctly fails when invoked for a non-existent rule.''' + with pytest.raises(InvalidArgumentError): + rule_creator.add_language_branch(101, 'cfamily') + +def test_add_language_new_pr(rule_creator: RuleCreator): + '''Test add_language_pull_request adds the right user and labels.''' + rule_number = 120 + language = 'php' + + ghMock = Mock() + ghRepoMock = Mock() + pullMock = Mock() + ghRepoMock.create_pull = Mock(return_value=pullMock) + ghMock.get_repo = Mock(return_value=ghRepoMock) + def mockGithub(user: Optional[str]): + return ghMock + + rule_creator.add_language_pull_request(mockGithub, rule_number, language, 'mylab', user='testuser') + + ghRepoMock.create_pull.assert_called_once(); + assert ghRepoMock.create_pull.call_args.kwargs['title'].startswith(f'Create rule S{rule_number}[{language}]') + ghRepoMock.create_pull.call_args.kwargs['head'].startswith('rule/') + pullMock.add_to_assignees.assert_called_with('testuser'); + pullMock.add_to_labels.assert_called_with('mylab'); diff --git a/rspec-tools/tests/test_rules_repository.py b/rspec-tools/tests/test_rules_repository.py index 0ae32663c5..16684fc2c7 100644 --- a/rspec-tools/tests/test_rules_repository.py +++ b/rspec-tools/tests/test_rules_repository.py @@ -8,7 +8,7 @@ from rspec_tools.rules import RulesRepository def test_list_rules(mockrules: Path): '''Check that rules are all listed.''' rules = {rule.id for rule in RulesRepository(rules_path=mockrules).rules} - assert rules == {'S100', 'S120', 'S4727'} + assert rules == {'S100', 'S120', 'S4727', 'S1033'} def test_list_languages(mockrules: Path): diff --git a/rspec-tools/tests/test_utils.py b/rspec-tools/tests/test_utils.py index 4f90963d05..383866615f 100644 --- a/rspec-tools/tests/test_utils.py +++ b/rspec-tools/tests/test_utils.py @@ -1,5 +1,5 @@ from rspec_tools.errors import InvalidArgumentError -from rspec_tools.utils import parse_and_validate_language_list, load_valid_languages, get_mapped_languages, get_labels_for_languages +from rspec_tools.utils import parse_and_validate_language_list, load_valid_languages, get_mapped_languages, get_labels_for_languages, resolve_rule, validate_language, get_label_for_language import pytest def test_fails_when_no_languages_listed(): @@ -31,3 +31,29 @@ def test_labels_for_languages(): lang_list = ['java', 'apex', 'cfamily', 'ruby'] labels = ['java', 'slang', 'cfamily'] assert set(get_labels_for_languages(lang_list)) == set(labels) + +def test_resolve_rule(): + assert resolve_rule('S100') == 100 + assert resolve_rule('S1234') == 1234 + with pytest.raises(InvalidArgumentError): + resolve_rule('S12') + with pytest.raises(InvalidArgumentError): + resolve_rule('12') + with pytest.raises(InvalidArgumentError): + resolve_rule('SS13') + with pytest.raises(InvalidArgumentError): + resolve_rule('RSPEC-1343') + with pytest.raises(InvalidArgumentError): + resolve_rule(' S1343 ') + with pytest.raises(InvalidArgumentError): + resolve_rule('SXXXX') + with pytest.raises(InvalidArgumentError): + resolve_rule('S90000') + +def test_label_for_language(): + assert get_label_for_language('java') == 'java' + +def test_validate_language(): + validate_language('java') + with pytest.raises(InvalidArgumentError): + validate_language('russian')